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

677 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 {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";
import "../../contracts/test/ERC4626/ERC4626Test.sol";
import {ProtocolFee} from "../../contracts/hooks/ProtocolFee.sol";
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);
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);
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));
}
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));
remoteMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
remoteToken.balanceOf(BOB),
transferAmount + _discountedYield(),
1e14,
0
);
}
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);
}
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
}
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"
);
}
function testWithdrawalWithoutYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
_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
);
}
function testWithdrawalWithYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
_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"
);
assertEq(vault.accumulatedFees(), YIELD / 10);
}
function testWithdrawalAfterYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
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,
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));
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);
_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));
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,
transferAmount - drawdown,
1e14,
0
);
}
function test_exchangeRate_setOnlyByCollateral() public {
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
_accrueYield();
localRebasingToken.rebase(DESTINATION, bytes(""), address(0));
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
assertEq(peerRebasingToken.exchangeRate(), 1e10); // assertingthat transfers by the synthetic variant don't impact the exchang rate
localRebasingToken.rebase(PEER_DESTINATION, bytes(""), address(0));
peerMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
peerRebasingToken.exchangeRate(),
1045e7,
1e14,
0
); // asserting that the exchange rate is set finally by the collateral variant
}
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
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));
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"
);
}
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));
remoteMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
remoteToken.balanceOf(BOB),
transferAmount + _discountedYield(),
1e14,
0
);
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();
}
}