The home for Hyperlane core contracts, sdk packages, and other infrastructure
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hyperlane-monorepo/solidity/test/token/HypERC4626Test.t.sol

678 lines
22 KiB

feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.13;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
import "forge-std/Test.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ERC4626Test} from "../../contracts/test/ERC4626/ERC4626Test.sol";
import {MockERC4626YieldSharing} from "../../contracts/mock/MockERC4626YieldSharing.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {HypTokenTest} from "./HypERC20.t.sol";
import {MockMailbox} from "../../contracts/mock/MockMailbox.sol";
import {HypERC20} from "../../contracts/token/HypERC20.sol";
import {HypERC4626Collateral} from "../../contracts/token/extensions/HypERC4626Collateral.sol";
import {HypERC4626} from "../../contracts/token/extensions/HypERC4626.sol";
import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol";
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
import "../../contracts/test/ERC4626/ERC4626Test.sol";
import {ProtocolFee} from "../../contracts/hooks/ProtocolFee.sol";
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
contract HypERC4626CollateralTest is HypTokenTest {
using TypeCasts for address;
uint32 internal constant PEER_DESTINATION = 13;
uint256 constant YIELD = 5e18;
uint256 constant YIELD_FEES = 1e17; // 10% of yield goes to the vault owner
uint256 internal transferAmount = 100e18;
HypERC4626Collateral internal rebasingCollateral;
MockERC4626YieldSharing vault;
MockMailbox internal peerMailbox; // mailbox for second synthetic token
HypERC20 internal peerToken;
HypERC4626Collateral localRebasingToken;
HypERC4626 remoteRebasingToken;
HypERC4626 peerRebasingToken;
event ExchangeRateUpdated(uint256 newExchangeRate, uint32 rateUpdateNonce);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
function setUp() public override {
super.setUp();
// multi-synthetic setup
peerMailbox = new MockMailbox(PEER_DESTINATION);
localMailbox.addRemoteMailbox(PEER_DESTINATION, peerMailbox);
remoteMailbox.addRemoteMailbox(PEER_DESTINATION, peerMailbox);
peerMailbox.addRemoteMailbox(DESTINATION, remoteMailbox);
peerMailbox.addRemoteMailbox(ORIGIN, localMailbox);
peerMailbox.setDefaultHook(address(noopHook));
peerMailbox.setRequiredHook(address(noopHook));
vm.prank(DANIEL); // daniel will be the owner of the vault and accrue yield fees
vault = new MockERC4626YieldSharing(
address(primaryToken),
"Regular Vault",
"RV",
YIELD_FEES
);
HypERC4626Collateral implementation = new HypERC4626Collateral(
vault,
address(localMailbox)
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
PROXY_ADMIN,
abi.encodeWithSelector(
HypERC4626Collateral.initialize.selector,
address(address(noopHook)),
address(0x0),
address(this)
)
);
localToken = HypERC4626Collateral(address(proxy));
remoteToken = new HypERC4626(
primaryToken.decimals(),
address(remoteMailbox),
localToken.localDomain()
);
peerToken = new HypERC4626(
primaryToken.decimals(),
address(peerMailbox),
localToken.localDomain()
);
localRebasingToken = HypERC4626Collateral(address(proxy));
remoteRebasingToken = HypERC4626(address(remoteToken));
peerRebasingToken = HypERC4626(address(peerToken));
primaryToken.transfer(ALICE, 1000e18);
primaryToken.transfer(BOB, 1000e18);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
uint32[] memory domains = new uint32[](3);
domains[0] = ORIGIN;
domains[1] = DESTINATION;
domains[2] = PEER_DESTINATION;
bytes32[] memory addresses = new bytes32[](3);
addresses[0] = address(localToken).addressToBytes32();
addresses[1] = address(remoteToken).addressToBytes32();
addresses[2] = address(peerToken).addressToBytes32();
_connectRouters(domains, addresses);
}
function testDisableInitializers() public {
vm.expectRevert("Initializable: contract is already initialized");
remoteToken.initialize(0, "", "", address(0), address(0), address(0));
}
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
function test_collateralDomain() public view {
assertEq(
remoteRebasingToken.collateralDomain(),
localToken.localDomain()
);
}
function testRemoteTransfer_rebaseAfter() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteToken.balanceOf(BOB),
transferAmount + _discountedYield(),
1e14,
0
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
);
}
function testRemoteTransfer_rebaseWithCustomHook() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
uint256 FEE = 1e18;
ProtocolFee customHook = new ProtocolFee(
FEE,
FEE,
address(this),
address(this)
);
localRebasingToken.rebase{value: FEE}(
DESTINATION,
StandardHookMetadata.overrideMsgValue(FEE),
address(customHook)
);
assertEq(address(customHook).balance, FEE);
}
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
function testRebaseWithTransfer() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
_performRemoteTransferWithoutExpectation(0, transferAmount);
// max 1bp diff
assertApproxEqRelDecimal(
remoteToken.balanceOf(BOB),
2 * transferAmount + _discountedYield(),
1e14,
0
);
}
function testRebase_exchangeRateUpdateInSequence() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
_accrueYield();
uint256 exchangeRateInitially = remoteRebasingToken.exchangeRate();
vm.startPrank(BOB);
primaryToken.approve(address(localToken), transferAmount);
localToken.transferRemote(
DESTINATION,
BOB.addressToBytes32(),
transferAmount
);
vm.stopPrank();
_accrueYield();
vm.startPrank(ALICE);
primaryToken.approve(address(localToken), transferAmount);
localToken.transferRemote(
DESTINATION,
BOB.addressToBytes32(),
transferAmount
);
vm.stopPrank();
// process ALICE's transfer
vm.expectEmit(true, true, true, true);
emit ExchangeRateUpdated(10721400472, 3);
remoteMailbox.processInboundMessage(2);
uint256 exchangeRateBefore = remoteRebasingToken.exchangeRate();
// process BOB's transfer
remoteMailbox.processInboundMessage(1);
uint256 exchangeRateAfter = remoteRebasingToken.exchangeRate();
assertLt(exchangeRateInitially, exchangeRateBefore); // updates bc nonce=2 is after nonce=0
assertEq(exchangeRateBefore, exchangeRateAfter); // doesn't update bc nonce=1 is before nonce=0
}
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
function testSyntheticTransfers_withRebase() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
_performRemoteTransferWithoutExpectation(0, transferAmount);
vm.prank(BOB);
remoteToken.transfer(CAROL, transferAmount); // transfer ~100e18 equivalent to CAROL
// max 1bp diff
assertApproxEqRelDecimal(
remoteToken.balanceOf(BOB),
transferAmount + _discountedYield(),
1e14,
0
);
assertApproxEqRelDecimal(
remoteToken.balanceOf(CAROL),
transferAmount,
1e14,
0
);
}
function testTransferFrom() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
uint256 transferAmount2 = 50e18;
vm.prank(BOB);
remoteToken.approve(CAROL, transferAmount2);
vm.prank(CAROL);
bool success = remoteToken.transferFrom(BOB, DANIEL, transferAmount2);
assertTrue(success, "TransferFrom should succeed");
assertEq(
remoteToken.balanceOf(BOB),
transferAmount - transferAmount2,
"BOB's balance should decrease"
);
assertEq(
remoteToken.balanceOf(DANIEL),
transferAmount2,
"DANIEL's balance should increase"
);
assertEq(
remoteToken.allowance(BOB, CAROL),
0,
"Allowance should be zero after transfer"
);
}
event Transfer(address indexed from, address indexed to, uint256 value);
function testTransferEvent() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
uint256 transferAmount2 = 50e18;
vm.expectEmit(true, true, false, true);
emit Transfer(BOB, CAROL, transferAmount2);
vm.prank(BOB);
remoteToken.transfer(CAROL, transferAmount2);
assertEq(
remoteToken.balanceOf(BOB),
transferAmount - transferAmount2,
"BOB's balance should decrease"
);
assertEq(
remoteToken.balanceOf(CAROL),
transferAmount2,
"CAROL's balance should increase"
);
}
function testTotalShares() public {
uint256 initialShares = remoteRebasingToken.totalShares();
assertEq(initialShares, 0, "Initial shares should be zero");
_performRemoteTransferWithoutExpectation(0, transferAmount);
uint256 sharesAfterTransfer = remoteRebasingToken.totalShares();
assertEq(
sharesAfterTransfer,
remoteRebasingToken.assetsToShares(transferAmount),
"Shares should match transferred amount converted to shares"
);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
remoteMailbox.processNextInboundMessage();
uint256 sharesAfterYield = remoteRebasingToken.totalShares();
assertEq(
sharesAfterYield,
sharesAfterTransfer,
"Total shares should remain constant after yield accrual"
);
}
function testShareBalanceOf() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
uint256 bobShareBalance = remoteRebasingToken.shareBalanceOf(BOB);
assertEq(
bobShareBalance,
remoteRebasingToken.assetsToShares(transferAmount),
"Bob's share balance should match transferred amount converted to shares"
);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
remoteMailbox.processNextInboundMessage();
uint256 bobShareBalanceAfterYield = remoteRebasingToken.shareBalanceOf(
BOB
);
assertEq(
bobShareBalanceAfterYield,
bobShareBalance,
"Bob's share balance should remain constant after yield accrual"
);
}
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
function testWithdrawalWithoutYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
vm.prank(BOB);
remoteToken.transferRemote{value: 0}(
ORIGIN,
BOB.addressToBytes32(),
transferAmount
);
localMailbox.processNextInboundMessage();
assertEq(
primaryToken.balanceOf(BOB) - bobPrimaryBefore,
transferAmount
);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
}
function testWithdrawalWithYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
vm.prank(BOB);
remoteToken.transferRemote{value: 0}(
ORIGIN,
BOB.addressToBytes32(),
transferAmount
);
localMailbox.processNextInboundMessage();
uint256 _bobBal = primaryToken.balanceOf(BOB);
uint256 _expectedBal = transferAmount + _discountedYield();
// BOB gets the yield even though it didn't rebase
assertApproxEqRelDecimal(
_bobBal - bobPrimaryBefore,
_expectedBal,
1e14,
0
);
assertTrue(
_bobBal - bobPrimaryBefore < _expectedBal,
"Transfer remote should round down"
);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
assertEq(vault.accumulatedFees(), YIELD / 10);
}
function testWithdrawalAfterYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteMailbox.processNextInboundMessage();
// Use balance here since it might be off by <1bp
uint256 bobsBalance = remoteToken.balanceOf(BOB);
vm.prank(BOB);
remoteToken.transferRemote{value: 0}(
ORIGIN,
BOB.addressToBytes32(),
bobsBalance
);
localMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
primaryToken.balanceOf(BOB) - bobPrimaryBefore,
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
transferAmount + _discountedYield(),
1e14,
0
);
assertEq(vault.accumulatedFees(), YIELD / 10);
}
function testWithdrawalInFlight() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
primaryToken.mintTo(CAROL, transferAmount);
vm.prank(CAROL);
primaryToken.approve(address(localToken), transferAmount);
vm.prank(CAROL);
localToken.transferRemote{value: 0}(
DESTINATION,
CAROL.addressToBytes32(),
transferAmount
);
remoteMailbox.processNextInboundMessage();
_accrueYield();
_accrueYield(); // earning 2x yield to be split
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
vm.prank(CAROL);
remoteToken.transferRemote(
ORIGIN,
CAROL.addressToBytes32(),
transferAmount
);
localMailbox.processNextInboundMessage();
uint256 claimableFees = vault.getClaimableFees();
assertApproxEqRelDecimal(
primaryToken.balanceOf(CAROL),
transferAmount + YIELD - (claimableFees / 2),
1e14,
0
);
// until we process the rebase, the yield is not added on the remote
assertEq(remoteToken.balanceOf(BOB), transferAmount);
remoteMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
remoteToken.balanceOf(BOB),
transferAmount + YIELD - (claimableFees / 2),
1e14,
0
);
assertEq(vault.accumulatedFees(), YIELD / 5); // 0.1 * 2 * yield
}
function testWithdrawalAfterDrawdown() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
// decrease collateral in vault by 10%
uint256 drawdown = 5e18;
primaryToken.burnFrom(address(vault), drawdown);
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteMailbox.processNextInboundMessage();
// Use balance here since it might be off by <1bp
uint256 bobsBalance = remoteToken.balanceOf(BOB);
vm.prank(BOB);
remoteToken.transferRemote{value: 0}(
ORIGIN,
BOB.addressToBytes32(),
bobsBalance
);
localMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
primaryToken.balanceOf(BOB) - bobPrimaryBefore,
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
transferAmount - drawdown,
1e14,
0
);
}
function test_exchangeRate_setOnlyByCollateral() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteMailbox.processNextInboundMessage();
vm.prank(BOB);
remoteToken.transferRemote{value: 0}(
PEER_DESTINATION,
BOB.addressToBytes32(),
transferAmount
);
peerMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
remoteRebasingToken.exchangeRate(),
1045e7,
1e14,
0
); // 5 * 0.9 = 4.5% yield
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
assertEq(peerRebasingToken.exchangeRate(), 1e10); // assertingthat transfers by the synthetic variant don't impact the exchang rate
localRebasingToken.rebase(PEER_DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
peerMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
peerRebasingToken.exchangeRate(),
1045e7,
1e14,
0
); // asserting that the exchange rate is set finally by the collateral variant
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
}
function test_cyclicTransfers() public {
// ALICE: local -> remote(BOB)
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0)); // yield is added
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteMailbox.processNextInboundMessage();
// BOB: remote -> peer(BOB) (yield is leftover)
vm.prank(BOB);
remoteToken.transferRemote{value: 0}(
PEER_DESTINATION,
BOB.addressToBytes32(),
transferAmount
);
peerMailbox.processNextInboundMessage();
localRebasingToken.rebase(PEER_DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
peerMailbox.processNextInboundMessage();
// BOB: peer -> local(CAROL)
vm.prank(BOB);
peerToken.transferRemote{value: 0}(
ORIGIN,
CAROL.addressToBytes32(),
transferAmount
);
localMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
remoteToken.balanceOf(BOB),
_discountedYield(),
1e14,
0
);
assertEq(peerToken.balanceOf(BOB), 0);
assertApproxEqRelDecimal(
primaryToken.balanceOf(CAROL),
transferAmount,
1e14,
0
);
}
function testTotalSupply() public {
uint256 initialSupply = remoteToken.totalSupply();
assertEq(initialSupply, 0, "Initial supply should be zero");
_performRemoteTransferWithoutExpectation(0, transferAmount);
uint256 supplyAfterTransfer = remoteToken.totalSupply();
assertEq(
supplyAfterTransfer,
transferAmount,
"Supply should match transferred amount"
);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
remoteMailbox.processNextInboundMessage();
uint256 supplyAfterYield = remoteToken.totalSupply();
assertApproxEqRelDecimal(
supplyAfterYield,
transferAmount + _discountedYield(),
1e14,
0,
"Supply should include yield"
);
}
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
function testTransfer_withHookSpecified(
uint256,
bytes calldata
) public override {
// skip
}
function testBenchmark_overheadGasUsage() public override {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
remoteToken.balanceOf(BOB),
transferAmount + _discountedYield(),
1e14,
0
feat: implementation of yield rebasing warp route contracts (#4203) ### Description - To reflect the rebased underlying asset, `_transferRemote` is overridden because we need to pass in the ERC4626 vault shares which on the synthetic `HypERC4626VaultYield` which change in value as the underlying vault shares accrue yield or receive a drawdown. - The exchangeRate (i.e., assets/shares) is encoded as metadata and sent to the synthetic token to calculate the value of the synthetic token. - On synthetic transfers, the overridden `ERC20.transfer()` function calculates the needed shares for the amount and transfers them instead. Design decisions - Do we need to rebase frequently? Yes, otherwise the exchangeRate on the synthetic token will be stale compared to the the actual vault claimable yield, which means a user who deposits later and right before the infrequent rebase will get disproportionate share of the yield. You can rebase by calling `rebase()` on the collateral token which makes a transferRemote call with 0 amount. - Why exchangeRate and not underlyingDepositedAmount on the synthetic? If a user withdraws (ie calls transferRemote on the synthetic) while a rebase is inflight, the underlyingDepositedAmount will get an inaccurate update (will be overreported and hence will derive inaccurate balance amounts on the synthetic. The exchangeRate doesn't change if the user is depositing or withdrawing. - Why override the `transfer` on the synthetic? Otherwise if a user A sends 100 USDC worth of shares to user B which has a exchange rate of 1.05, user A would have unwittingly sent 105 #tokens (5 excess). - What happens in a drawdown of the value? Once the user calls withdraws the the withdrawal they get out is accordingly discounted. If a user transfers on the synthetic, it will still gets the stale exchangeRate until rebase. - What if the owner of the vault charges a fee on the yield? In that case, you need to override the totalAssets to reflect the assets - feesByOwner and the warp route implementation will stay the same. One example implementation is `MockERC4626YieldSharing` and we have adequate tests to cover the functionality. - Rounding error? We're rounding down the exchangeRate as per the general recommendation for ERC4626 vault math. - Why not make the synthetic token an ERC4626 itself, given that there's some overlapping functionality? Even though, the assetsToShares and assetsToShares functions are the same, the synthetic token itself doesn't have an underlying token so it doesn't confer to the 4626 interface. - What if there are multiple synthetic tokens for a single rebasing collateral contract and what happens is the exchange rate is not in sync between the synthetics? Things to keep in mind for - How do I keep the exchangeRate in sync with the underlying vault? Keep calling rebase as a cron job frequently, or make a hook call in deposit, redeem, and withdraw function to automatically resync the exchangeRate. - 4626 math pitfalls: https://www.zellic.io/blog/exploring-erc-4626/ ### Drive-by changes - Renaming `HypERC20CollateralVaultDeposit` -> `HypERC4626OwnerYieldCollateral` ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1334 ### Backward compatibility Yes ### Testing Unit tests
4 months ago
);
vm.prank(address(localMailbox));
uint256 gasBefore = gasleft();
localToken.handle(
DESTINATION,
address(remoteToken).addressToBytes32(),
abi.encodePacked(BOB.addressToBytes32(), transferAmount)
);
uint256 gasAfter = gasleft();
console.log(
"Overhead gas usage for withdrawal: %d",
gasBefore - gasAfter
);
}
// ALICE: local -> remote(BOB)
function _performRemoteTransferWithoutExpectation(
uint256 _msgValue,
uint256 _amount
) internal {
vm.startPrank(ALICE);
primaryToken.approve(address(localToken), transferAmount);
localToken.transferRemote{value: _msgValue}(
DESTINATION,
BOB.addressToBytes32(),
_amount
);
vm.stopPrank();
remoteMailbox.processNextInboundMessage();
}
function _accrueYield() public {
primaryToken.mintTo(address(vault), YIELD);
}
function _discountedYield() internal view returns (uint256) {
return YIELD - vault.getClaimableFees();
}
}