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.
506 lines
16 KiB
506 lines
16 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 "../../contracts/test/ERC4626/ERC4626Test.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 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);
|
|
remoteMailbox.processNextInboundMessage();
|
|
assertEq(
|
|
remoteToken.balanceOf(BOB),
|
|
transferAmount + _discountedYield()
|
|
);
|
|
}
|
|
|
|
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 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);
|
|
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);
|
|
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);
|
|
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);
|
|
remoteMailbox.processNextInboundMessage();
|
|
|
|
vm.prank(BOB);
|
|
remoteToken.transferRemote{value: 0}(
|
|
PEER_DESTINATION,
|
|
BOB.addressToBytes32(),
|
|
transferAmount
|
|
);
|
|
peerMailbox.processNextInboundMessage();
|
|
|
|
assertEq(remoteRebasingToken.exchangeRate(), 1045e7); // 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);
|
|
peerMailbox.processNextInboundMessage();
|
|
|
|
assertEq(peerRebasingToken.exchangeRate(), 1045e7); // 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); // 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);
|
|
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 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);
|
|
remoteMailbox.processNextInboundMessage();
|
|
assertEq(
|
|
remoteToken.balanceOf(BOB),
|
|
transferAmount + _discountedYield()
|
|
);
|
|
|
|
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();
|
|
}
|
|
}
|
|
|