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/HypERC20.t.sol

719 lines
22 KiB

// 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 {Mailbox} from "../../contracts/Mailbox.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.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 {MockMailbox} from "../../contracts/mock/MockMailbox.sol";
import {XERC20LockboxTest, XERC20Test, FiatTokenTest, ERC20Test} from "../../contracts/test/ERC20Test.sol";
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol";
import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol";
import {GasRouter} from "../../contracts/client/GasRouter.sol";
import {IPostDispatchHook} from "../../contracts/interfaces/hooks/IPostDispatchHook.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 {Router} from "../../contracts/client/Router.sol";
import {HypERC20} from "../../contracts/token/HypERC20.sol";
import {HypERC20Collateral} from "../../contracts/token/HypERC20Collateral.sol";
import {HypXERC20Lockbox} from "../../contracts/token/extensions/HypXERC20Lockbox.sol";
import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol";
import {IFiatToken} from "../../contracts/token/interfaces/IFiatToken.sol";
import {HypXERC20} from "../../contracts/token/extensions/HypXERC20.sol";
import {HypFiatToken} from "../../contracts/token/extensions/HypFiatToken.sol";
import {HypNative} from "../../contracts/token/HypNative.sol";
import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol";
import {TokenMessage} from "../../contracts/token/libs/TokenMessage.sol";
import {Message} from "../../contracts/libs/Message.sol";
abstract contract HypTokenTest is Test {
using TypeCasts for address;
using TokenMessage for bytes;
using Message for bytes;
uint32 internal constant ORIGIN = 11;
uint32 internal constant DESTINATION = 12;
uint8 internal constant DECIMALS = 18;
uint256 internal constant TOTAL_SUPPLY = 1_000_000e18;
uint256 internal REQUIRED_VALUE; // initialized in setUp
uint256 internal constant GAS_LIMIT = 10_000;
uint256 internal TRANSFER_AMT = 100e18;
string internal constant NAME = "HyperlaneInu";
string internal constant SYMBOL = "HYP";
address internal constant ALICE = address(0x1);
address internal constant BOB = address(0x2);
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
address internal constant CAROL = address(0x3);
address internal constant DANIEL = address(0x4);
address internal constant PROXY_ADMIN = address(0x37);
ERC20Test internal primaryToken;
TokenRouter internal localToken;
HypERC20 internal remoteToken;
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
MockMailbox internal localMailbox;
MockMailbox internal remoteMailbox;
TestPostDispatchHook internal noopHook;
TestInterchainGasPaymaster internal igp;
event SentTransferRemote(
uint32 indexed destination,
bytes32 indexed recipient,
uint256 amount
);
event ReceivedTransferRemote(
uint32 indexed origin,
bytes32 indexed recipient,
uint256 amount
);
function setUp() public virtual {
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
localMailbox = new MockMailbox(ORIGIN);
remoteMailbox = new MockMailbox(DESTINATION);
localMailbox.addRemoteMailbox(DESTINATION, remoteMailbox);
remoteMailbox.addRemoteMailbox(ORIGIN, localMailbox);
primaryToken = new ERC20Test(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS);
noopHook = new TestPostDispatchHook();
localMailbox.setDefaultHook(address(noopHook));
localMailbox.setRequiredHook(address(noopHook));
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.setDefaultHook(address(noopHook));
remoteMailbox.setRequiredHook(address(noopHook));
REQUIRED_VALUE = noopHook.quoteDispatch("", "");
HypERC20 implementation = new HypERC20(
DECIMALS,
address(remoteMailbox)
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
PROXY_ADMIN,
abi.encodeWithSelector(
HypERC20.initialize.selector,
TOTAL_SUPPLY,
NAME,
SYMBOL,
address(noopHook),
address(igp),
address(this)
)
);
remoteToken = HypERC20(address(proxy));
remoteToken.enrollRemoteRouter(
ORIGIN,
address(localToken).addressToBytes32()
);
igp = new TestInterchainGasPaymaster();
vm.deal(ALICE, 125000);
}
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 _enrollLocalTokenRouter() internal {
localToken.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
}
function _enrollRemoteTokenRouter() internal {
remoteToken.enrollRemoteRouter(
ORIGIN,
address(localToken).addressToBytes32()
);
}
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 _connectRouters(
uint32[] memory _domains,
bytes32[] memory _addresses
) internal {
uint256 n = _domains.length;
for (uint256 i = 0; i < n; i++) {
uint32[] memory complementDomains = new uint32[](n - 1);
bytes32[] memory complementAddresses = new bytes32[](n - 1);
uint256 j = 0;
for (uint256 k = 0; k < n; k++) {
if (k != i) {
complementDomains[j] = _domains[k];
complementAddresses[j] = _addresses[k];
j++;
}
}
// Enroll complement routers into the current router, Routers - router_i
Router(TypeCasts.bytes32ToAddress(_addresses[i]))
.enrollRemoteRouters(complementDomains, complementAddresses);
}
}
function _expectRemoteBalance(
address _user,
uint256 _balance
) internal view {
assertEq(remoteToken.balanceOf(_user), _balance);
}
function _processTransfers(address _recipient, uint256 _amount) internal {
vm.prank(address(remoteMailbox));
remoteToken.handle(
ORIGIN,
address(localToken).addressToBytes32(),
abi.encodePacked(_recipient.addressToBytes32(), _amount)
);
}
function _handleLocalTransfer(uint256 _transferAmount) internal {
vm.prank(address(localMailbox));
localToken.handle(
DESTINATION,
address(remoteToken).addressToBytes32(),
abi.encodePacked(ALICE.addressToBytes32(), _transferAmount)
);
}
function _mintAndApprove(uint256 _amount, address _account) internal {
primaryToken.mint(_amount);
primaryToken.approve(_account, _amount);
}
function _setCustomGasConfig() internal {
localToken.setHook(address(igp));
TokenRouter.GasRouterConfig[]
memory config = new TokenRouter.GasRouterConfig[](1);
config[0] = GasRouter.GasRouterConfig({
domain: DESTINATION,
gas: GAS_LIMIT
});
localToken.setDestinationGas(config);
}
function _performRemoteTransfer(
uint256 _msgValue,
uint256 _amount
) internal {
vm.prank(ALICE);
localToken.transferRemote{value: _msgValue}(
DESTINATION,
BOB.addressToBytes32(),
_amount
);
vm.expectEmit(true, true, false, true);
emit ReceivedTransferRemote(ORIGIN, BOB.addressToBytes32(), _amount);
_processTransfers(BOB, _amount);
assertEq(remoteToken.balanceOf(BOB), _amount);
}
function _performRemoteTransferAndGas(
uint256 _msgValue,
uint256 _amount,
uint256 _gasOverhead
) internal {
uint256 ethBalance = ALICE.balance;
_performRemoteTransfer(_msgValue + _gasOverhead, _amount);
assertEq(ALICE.balance, ethBalance - REQUIRED_VALUE - _gasOverhead);
}
function _performRemoteTransferWithEmit(
uint256 _msgValue,
uint256 _amount,
uint256 _gasOverhead
) internal {
vm.expectEmit(true, true, false, true);
emit SentTransferRemote(DESTINATION, BOB.addressToBytes32(), _amount);
_performRemoteTransferAndGas(_msgValue, _amount, _gasOverhead);
}
function _performRemoteTransferWithHook(
uint256 _msgValue,
uint256 _amount,
address _hook,
bytes memory _hookMetadata
) internal returns (bytes32 messageId) {
vm.prank(ALICE);
messageId = localToken.transferRemote{value: _msgValue}(
DESTINATION,
BOB.addressToBytes32(),
_amount,
_hookMetadata,
address(_hook)
);
_processTransfers(BOB, _amount);
assertEq(remoteToken.balanceOf(BOB), _amount);
}
function testTransfer_withHookSpecified(
uint256 fee,
bytes calldata metadata
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
) public virtual {
TestPostDispatchHook hook = new TestPostDispatchHook();
hook.setFee(fee);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
bytes32 messageId = _performRemoteTransferWithHook(
REQUIRED_VALUE,
TRANSFER_AMT,
address(hook),
metadata
);
assertTrue(hook.messageDispatched(messageId));
}
function testBenchmark_overheadGasUsage() public virtual {
vm.prank(address(localMailbox));
uint256 gasBefore = gasleft();
localToken.handle(
DESTINATION,
address(remoteToken).addressToBytes32(),
abi.encodePacked(BOB.addressToBytes32(), TRANSFER_AMT)
);
uint256 gasAfter = gasleft();
console.log("Overhead gas usage: %d", gasBefore - gasAfter);
}
}
contract HypERC20Test is HypTokenTest {
using TypeCasts for address;
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
HypERC20 internal erc20Token;
function setUp() public override {
super.setUp();
HypERC20 implementation = new HypERC20(DECIMALS, address(localMailbox));
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
PROXY_ADMIN,
abi.encodeWithSelector(
HypERC20.initialize.selector,
TOTAL_SUPPLY,
NAME,
SYMBOL,
address(address(noopHook)),
address(igp),
address(this)
)
);
localToken = HypERC20(address(proxy));
erc20Token = HypERC20(address(proxy));
erc20Token.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
erc20Token.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function testInitialize_revert_ifAlreadyInitialized() public {
vm.expectRevert("Initializable: contract is already initialized");
erc20Token.initialize(
TOTAL_SUPPLY,
NAME,
SYMBOL,
address(address(noopHook)),
address(igp),
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
function testTotalSupply() public view {
assertEq(erc20Token.totalSupply(), TOTAL_SUPPLY);
}
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 testDecimals() public view {
assertEq(erc20Token.decimals(), DECIMALS);
}
function testLocalTransfers() public {
assertEq(erc20Token.balanceOf(ALICE), 1000e18);
assertEq(erc20Token.balanceOf(BOB), 0);
vm.prank(ALICE);
erc20Token.transfer(BOB, 100e18);
assertEq(erc20Token.balanceOf(ALICE), 900e18);
assertEq(erc20Token.balanceOf(BOB), 100e18);
}
function testRemoteTransfer() public {
remoteToken.enrollRemoteRouter(
ORIGIN,
address(localToken).addressToBytes32()
);
uint256 balanceBefore = erc20Token.balanceOf(ALICE);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(erc20Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testRemoteTransfer_invalidAmount() public {
vm.expectRevert("ERC20: burn amount exceeds balance");
_performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMT * 11);
assertEq(erc20Token.balanceOf(ALICE), 1000e18);
}
function testRemoteTransfer_withCustomGasConfig() public {
_setCustomGasConfig();
uint256 balanceBefore = erc20Token.balanceOf(ALICE);
_performRemoteTransferAndGas(
REQUIRED_VALUE,
TRANSFER_AMT,
GAS_LIMIT * igp.gasPrice()
);
assertEq(erc20Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
}
contract HypERC20CollateralTest is HypTokenTest {
using TypeCasts for address;
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
HypERC20Collateral internal erc20Collateral;
function setUp() public override {
super.setUp();
localToken = new HypERC20Collateral(
address(primaryToken),
address(localMailbox)
);
erc20Collateral = HypERC20Collateral(address(localToken));
erc20Collateral.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
primaryToken.transfer(address(localToken), 1000e18);
primaryToken.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function test_constructor_revert_ifInvalidToken() public {
vm.expectRevert("HypERC20Collateral: invalid token");
new HypERC20Collateral(address(0), address(localMailbox));
}
function testInitialize_revert_ifAlreadyInitialized() public {}
function testRemoteTransfer() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testRemoteTransfer_invalidAllowance() public {
vm.expectRevert("ERC20: insufficient allowance");
_performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMT);
assertEq(localToken.balanceOf(ALICE), 1000e18);
}
function testRemoteTransfer_withCustomGasConfig() public {
_setCustomGasConfig();
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
_performRemoteTransferAndGas(
REQUIRED_VALUE,
TRANSFER_AMT,
GAS_LIMIT * igp.gasPrice()
);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
}
contract HypXERC20Test is HypTokenTest {
using TypeCasts for address;
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
HypXERC20 internal xerc20Collateral;
function setUp() public override {
super.setUp();
primaryToken = new XERC20Test(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS);
localToken = new HypXERC20(
address(primaryToken),
address(localMailbox)
);
xerc20Collateral = HypXERC20(address(localToken));
xerc20Collateral.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
primaryToken.transfer(address(localToken), 1000e18);
primaryToken.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function testRemoteTransfer() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
vm.expectCall(
address(primaryToken),
abi.encodeCall(IXERC20.burn, (ALICE, TRANSFER_AMT))
);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testHandle() public {
vm.expectCall(
address(primaryToken),
abi.encodeCall(IXERC20.mint, (ALICE, TRANSFER_AMT))
);
_handleLocalTransfer(TRANSFER_AMT);
}
}
contract HypXERC20LockboxTest is HypTokenTest {
using TypeCasts for address;
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
HypXERC20Lockbox internal xerc20Lockbox;
function setUp() public override {
super.setUp();
XERC20LockboxTest lockbox = new XERC20LockboxTest(
NAME,
SYMBOL,
TOTAL_SUPPLY,
DECIMALS
);
primaryToken = ERC20Test(address(lockbox.ERC20()));
localToken = new HypXERC20Lockbox(
address(lockbox),
address(localMailbox)
);
xerc20Lockbox = HypXERC20Lockbox(address(localToken));
xerc20Lockbox.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
primaryToken.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
uint256 constant MAX_INT = 2 ** 256 - 1;
function testApproval() public {
assertEq(
xerc20Lockbox.xERC20().allowance(
address(localToken),
address(xerc20Lockbox.lockbox())
),
MAX_INT
);
assertEq(
xerc20Lockbox.wrappedToken().allowance(
address(localToken),
address(xerc20Lockbox.lockbox())
),
MAX_INT
);
}
function testRemoteTransfer() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
vm.expectCall(
address(xerc20Lockbox.xERC20()),
abi.encodeCall(IXERC20.burn, (address(localToken), TRANSFER_AMT))
);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testHandle() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.expectCall(
address(xerc20Lockbox.xERC20()),
abi.encodeCall(IXERC20.mint, (address(localToken), TRANSFER_AMT))
);
_handleLocalTransfer(TRANSFER_AMT);
assertEq(localToken.balanceOf(ALICE), balanceBefore + TRANSFER_AMT);
}
}
contract HypFiatTokenTest is HypTokenTest {
using TypeCasts for address;
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
HypFiatToken internal fiatToken;
function setUp() public override {
super.setUp();
primaryToken = new FiatTokenTest(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS);
localToken = new HypFiatToken(
address(primaryToken),
address(localMailbox)
);
fiatToken = HypFiatToken(address(localToken));
fiatToken.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
primaryToken.transfer(address(localToken), 1000e18);
primaryToken.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function testRemoteTransfer() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
vm.expectCall(
address(primaryToken),
abi.encodeCall(IFiatToken.burn, (TRANSFER_AMT))
);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testHandle() public {
bytes memory data = abi.encodeCall(
IFiatToken.mint,
(ALICE, TRANSFER_AMT)
);
vm.mockCall(address(primaryToken), 0, data, abi.encode(false));
vm.expectRevert("FiatToken mint failed");
_handleLocalTransfer(TRANSFER_AMT);
vm.clearMockedCalls();
vm.expectCall(address(primaryToken), data);
_handleLocalTransfer(TRANSFER_AMT);
}
}
contract HypNativeTest is HypTokenTest {
using TypeCasts for address;
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
HypNative internal nativeToken;
function setUp() public override {
super.setUp();
localToken = new HypNative(address(localMailbox));
nativeToken = HypNative(payable(address(localToken)));
nativeToken.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
vm.deal(address(localToken), 1000e18);
vm.deal(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function testTransfer_withHookSpecified(
uint256 fee,
bytes calldata metadata
) public override {
TestPostDispatchHook hook = new TestPostDispatchHook();
hook.setFee(fee);
uint256 value = REQUIRED_VALUE + TRANSFER_AMT;
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
bytes32 messageId = _performRemoteTransferWithHook(
value,
TRANSFER_AMT,
address(hook),
metadata
);
assertTrue(hook.messageDispatched(messageId));
}
function testRemoteTransfer() public {
_performRemoteTransferWithEmit(
REQUIRED_VALUE,
TRANSFER_AMT,
TRANSFER_AMT
);
}
function testRemoteTransfer_invalidAmount() public {
vm.expectRevert("Native: amount exceeds msg.value");
_performRemoteTransfer(
REQUIRED_VALUE + TRANSFER_AMT,
TRANSFER_AMT * 10
);
assertEq(localToken.balanceOf(ALICE), 1000e18);
}
function testRemoteTransfer_withCustomGasConfig() public {
_setCustomGasConfig();
_performRemoteTransferAndGas(
REQUIRED_VALUE,
TRANSFER_AMT,
TRANSFER_AMT + GAS_LIMIT * igp.gasPrice()
);
}
function test_transferRemote_reverts_whenAmountExceedsValue(
uint256 nativeValue
) public {
vm.assume(nativeValue < address(this).balance);
address recipient = address(0xdeadbeef);
bytes32 bRecipient = TypeCasts.addressToBytes32(recipient);
vm.expectRevert("Native: amount exceeds msg.value");
nativeToken.transferRemote{value: nativeValue}(
DESTINATION,
bRecipient,
nativeValue + 1
);
vm.expectRevert("Native: amount exceeds msg.value");
nativeToken.transferRemote{value: nativeValue}(
DESTINATION,
bRecipient,
nativeValue + 1,
bytes(""),
address(0)
);
}
}