feat(contracts): add nonce for monotonically increasing delivery ordering for `HypERC4626` (#4534)

### Description

- Added a `rateUpdateNonce` in `HypERC4626Collateral` and
`previousNonce` in `HypERC4626` to ensure we only update the
exchangeRate on the synthetic asset if the update was after the last
recorded update. This is to make sure we don't update it to a stale
exchange rate which may cause losses to users using the synthetic asset.

### Drive-by changes

- `processInboundMessage` in MockMailbox` to simulate processing of
messages out of order.

### Related issues

- fixes https://github.com/chainlight-io/2024-08-hyperlane/issues/12

### Backward compatibility

Yes

### Testing

Unit test
pull/4724/head
Kunal Arora 1 month ago committed by GitHub
parent 2760da1ded
commit ec6b874b15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/red-actors-shop.md
  2. 5
      solidity/contracts/mock/MockMailbox.sol
  3. 14
      solidity/contracts/token/extensions/HypERC4626.sol
  4. 9
      solidity/contracts/token/extensions/HypERC4626Collateral.sol
  5. 69
      solidity/test/token/HypERC4626Test.t.sol

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/core': patch
---
Added nonce to HypERC4626

@ -77,4 +77,9 @@ contract MockMailbox is Mailbox {
Mailbox(address(this)).process{value: msg.value}("", _message);
inboundProcessedNonce++;
}
function processInboundMessage(uint32 _nonce) public {
bytes memory _message = inboundMessages[_nonce];
Mailbox(address(this)).process("", _message);
}
}

@ -17,9 +17,12 @@ contract HypERC4626 is HypERC20 {
using Message for bytes;
using TokenMessage for bytes;
event ExchangeRateUpdated(uint256 newExchangeRate, uint32 rateUpdateNonce);
uint256 public constant PRECISION = 1e10;
uint32 public immutable collateralDomain;
uint256 public exchangeRate; // 1e10
uint32 public previousNonce;
constructor(
uint8 _decimals,
@ -66,7 +69,16 @@ contract HypERC4626 is HypERC20 {
bytes calldata _message
) internal virtual override {
if (_origin == collateralDomain) {
exchangeRate = abi.decode(_message.metadata(), (uint256));
(uint256 newExchangeRate, uint32 rateUpdateNonce) = abi.decode(
_message.metadata(),
(uint256, uint32)
);
// only update if the nonce is greater than the previous nonce
if (rateUpdateNonce > previousNonce) {
exchangeRate = newExchangeRate;
previousNonce = rateUpdateNonce;
emit ExchangeRateUpdated(exchangeRate, rateUpdateNonce);
}
}
super._handle(_origin, _sender, _message);
}

@ -20,6 +20,8 @@ contract HypERC4626Collateral is HypERC20Collateral {
uint256 public constant PRECISION = 1e10;
bytes32 public constant NULL_RECIPIENT =
0x0000000000000000000000000000000000000000000000000000000000000001;
// Nonce for the rate update, to ensure sequential updates
uint32 public rateUpdateNonce;
constructor(
ERC4626 _vault,
@ -52,7 +54,12 @@ contract HypERC4626Collateral is HypERC20Collateral {
vault.totalSupply(),
Math.Rounding.Down
);
bytes memory _tokenMetadata = abi.encode(_exchangeRate);
rateUpdateNonce++;
bytes memory _tokenMetadata = abi.encode(
_exchangeRate,
rateUpdateNonce
);
bytes memory _tokenMessage = TokenMessage.format(
_recipient,

@ -43,6 +43,8 @@ contract HypERC4626CollateralTest is HypTokenTest {
HypERC4626 remoteRebasingToken;
HypERC4626 peerRebasingToken;
event ExchangeRateUpdated(uint256 newExchangeRate, uint32 rateUpdateNonce);
function setUp() public override {
super.setUp();
@ -95,6 +97,7 @@ contract HypERC4626CollateralTest is HypTokenTest {
peerRebasingToken = HypERC4626(address(peerToken));
primaryToken.transfer(ALICE, 1000e18);
primaryToken.transfer(BOB, 1000e18);
uint32[] memory domains = new uint32[](3);
domains[0] = ORIGIN;
@ -146,6 +149,47 @@ contract HypERC4626CollateralTest is HypTokenTest {
);
}
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);
@ -173,6 +217,7 @@ contract HypERC4626CollateralTest is HypTokenTest {
}
function testWithdrawalWithoutYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
@ -183,10 +228,14 @@ contract HypERC4626CollateralTest is HypTokenTest {
transferAmount
);
localMailbox.processNextInboundMessage();
assertEq(primaryToken.balanceOf(BOB), transferAmount);
assertEq(
primaryToken.balanceOf(BOB) - bobPrimaryBefore,
transferAmount
);
}
function testWithdrawalWithYield() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
@ -205,13 +254,22 @@ contract HypERC4626CollateralTest is HypTokenTest {
uint256 _expectedBal = transferAmount + _discountedYield();
// BOB gets the yield even though it didn't rebase
assertApproxEqRelDecimal(_bobBal, _expectedBal, 1e14, 0);
assertTrue(_bobBal < _expectedBal, "Transfer remote should round down");
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);
@ -230,7 +288,7 @@ contract HypERC4626CollateralTest is HypTokenTest {
);
localMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
primaryToken.balanceOf(BOB),
primaryToken.balanceOf(BOB) - bobPrimaryBefore,
transferAmount + _discountedYield(),
1e14,
0
@ -287,6 +345,7 @@ contract HypERC4626CollateralTest is HypTokenTest {
}
function testWithdrawalAfterDrawdown() public {
uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB);
_performRemoteTransferWithoutExpectation(0, transferAmount);
assertEq(remoteToken.balanceOf(BOB), transferAmount);
@ -306,7 +365,7 @@ contract HypERC4626CollateralTest is HypTokenTest {
);
localMailbox.processNextInboundMessage();
assertApproxEqRelDecimal(
primaryToken.balanceOf(BOB),
primaryToken.balanceOf(BOB) - bobPrimaryBefore,
transferAmount - drawdown,
1e14,
0

Loading…
Cancel
Save