From 1af6088d702218846cdbebad81eeccf4de301d9b Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Thu, 24 Oct 2024 00:31:56 +0530 Subject: [PATCH] feat(contracts): add wrapped HypERC4626 for ease of defi use (#4563) ### Description ### Drive-by changes None ### Related issues - closes https://github.com/chainlight-io/2024-08-hyperlane/issues/7 ### Backward compatibility Yes ### Testing Unit --- .changeset/itchy-bananas-know.md | 5 + .changeset/shiny-baboons-hunt.md | 5 + .../token/extensions/WHypERC4626.sol | 113 +++++++++++++++++ solidity/test/token/WHypERC4626.t.sol | 116 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 .changeset/itchy-bananas-know.md create mode 100644 .changeset/shiny-baboons-hunt.md create mode 100644 solidity/contracts/token/extensions/WHypERC4626.sol create mode 100644 solidity/test/token/WHypERC4626.t.sol diff --git a/.changeset/itchy-bananas-know.md b/.changeset/itchy-bananas-know.md new file mode 100644 index 0000000000..1889817569 --- /dev/null +++ b/.changeset/itchy-bananas-know.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': patch +--- + +Add wrapped HypERC4626 for easy defi use diff --git a/.changeset/shiny-baboons-hunt.md b/.changeset/shiny-baboons-hunt.md new file mode 100644 index 0000000000..ef302d93fa --- /dev/null +++ b/.changeset/shiny-baboons-hunt.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': minor +--- + +Added WHypERC4626 as a wrapper for rebasing HypERC4626 diff --git a/solidity/contracts/token/extensions/WHypERC4626.sol b/solidity/contracts/token/extensions/WHypERC4626.sol new file mode 100644 index 0000000000..08cb597321 --- /dev/null +++ b/solidity/contracts/token/extensions/WHypERC4626.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {HypERC4626} from "./HypERC4626.sol"; +import {PackageVersioned} from "../../PackageVersioned.sol"; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +/** + * @title WHypERC4626 + * @author Abacus Works + * @notice A wrapper for HypERC4626 that allows for wrapping and unwrapping of underlying rebasing tokens + */ +contract WHypERC4626 is ERC20, PackageVersioned { + HypERC4626 public immutable underlying; + + constructor( + HypERC4626 _underlying, + string memory name, + string memory symbol + ) ERC20(name, symbol) { + underlying = _underlying; + } + + /* + * @notice Wraps an amount of underlying tokens into wrapped tokens + * @param _underlyingAmount The amount of underlying tokens to wrap + * @return The amount of wrapped tokens + */ + function wrap(uint256 _underlyingAmount) external returns (uint256) { + require( + _underlyingAmount > 0, + "WHypERC4626: wrap amount must be greater than 0" + ); + uint256 wrappedAmount = underlying.assetsToShares(_underlyingAmount); + _mint(msg.sender, wrappedAmount); + underlying.transferFrom(msg.sender, address(this), _underlyingAmount); + return wrappedAmount; + } + + /* + * @notice Unwraps an amount of wrapped tokens into underlying tokens + * @param _wrappedAmount The amount of wrapped tokens to unwrap + * @return The amount of underlying tokens + */ + function unwrap(uint256 _wrappedAmount) external returns (uint256) { + require( + _wrappedAmount > 0, + "WHypERC4626: unwrap amount must be greater than 0" + ); + uint256 underlyingAmount = underlying.sharesToAssets(_wrappedAmount); + _burn(msg.sender, _wrappedAmount); + underlying.transfer(msg.sender, underlyingAmount); + return underlyingAmount; + } + + /* + * @notice Gets the amount of wrapped tokens for a given amount of underlying tokens + * @param _underlyingAmount The amount of underlying tokens + * @return The amount of wrapped tokens + */ + function getWrappedAmount( + uint256 _underlyingAmount + ) external view returns (uint256) { + return underlying.assetsToShares(_underlyingAmount); + } + + /* + * @notice Gets the amount of underlying tokens for a given amount of wrapped tokens + * @param _wrappedAmount The amount of wrapped tokens + * @return The amount of underlying tokens + */ + function getUnderlyingAmount( + uint256 _wrappedAmount + ) external view returns (uint256) { + return underlying.sharesToAssets(_wrappedAmount); + } + + /* + * @notice Gets the amount of wrapped tokens for 1 unit of underlying tokens + * @return The amount of wrapped tokens + */ + function wrappedPerUnderlying() external view returns (uint256) { + return underlying.assetsToShares(1 * 10 ** underlying.decimals()); + } + + /* + * @notice Gets the amount of underlying tokens for 1 unit of wrapped tokens + * @return The amount of underlying tokens + */ + function underlyingPerWrapped() external view returns (uint256) { + return underlying.sharesToAssets(1 * 10 ** decimals()); + } + + /* + * @notice Gets the decimals of the wrapped token + * @return The decimals of the wrapped token + */ + function decimals() public view override returns (uint8) { + return underlying.decimals(); + } +} diff --git a/solidity/test/token/WHypERC4626.t.sol b/solidity/test/token/WHypERC4626.t.sol new file mode 100644 index 0000000000..5684eeee28 --- /dev/null +++ b/solidity/test/token/WHypERC4626.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {MockMailbox} from "../../contracts/mock/MockMailbox.sol"; +import {WHypERC4626} from "../../contracts/token/extensions/WHypERC4626.sol"; +import {HypERC4626} from "../../contracts/token/extensions/HypERC4626.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockHypERC4626 is HypERC4626 { + constructor(address _mailbox) HypERC4626(18, _mailbox, 2) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract WHypERC4626Test is Test { + WHypERC4626 public wHypERC4626; + MockHypERC4626 public underlyingToken; + address public alice = address(0x1); + address public bob = address(0x2); + + function setUp() public { + MockMailbox mailbox = new MockMailbox(1); + underlyingToken = new MockHypERC4626(address(mailbox)); + wHypERC4626 = new WHypERC4626( + underlyingToken, + "Wrapped Rebasing Token", + "WRT" + ); + + underlyingToken.mint(alice, 1000 * 10 ** 18); + underlyingToken.mint(bob, 1000 * 10 ** 18); + } + + function test_wrap() public { + uint256 amount = 100 * 10 ** 18; + + vm.startPrank(alice); + underlyingToken.approve(address(wHypERC4626), amount); + uint256 wrappedAmount = wHypERC4626.wrap(amount); + + assertEq(wHypERC4626.balanceOf(alice), wrappedAmount); + assertEq(underlyingToken.balanceOf(alice), 900 * 10 ** 18); + vm.stopPrank(); + } + + function test_wrap_revertsWhen_zeroAmount() public { + vm.startPrank(alice); + underlyingToken.approve(address(wHypERC4626), 0); + vm.expectRevert("WHypERC4626: wrap amount must be greater than 0"); + wHypERC4626.wrap(0); + vm.stopPrank(); + } + + function test_unwrap() public { + uint256 amount = 100 * 10 ** 18; + + vm.startPrank(alice); + underlyingToken.approve(address(wHypERC4626), amount); + uint256 wrappedAmount = wHypERC4626.wrap(amount); + + uint256 unwrappedAmount = wHypERC4626.unwrap(wrappedAmount); + + assertEq(wHypERC4626.balanceOf(alice), 0); + assertEq(underlyingToken.balanceOf(alice), 1000 * 10 ** 18); + assertEq(unwrappedAmount, amount); + vm.stopPrank(); + } + + function test_unwrap_revertsWhen_zeroAmount() public { + vm.startPrank(alice); + vm.expectRevert("WHypERC4626: unwrap amount must be greater than 0"); + wHypERC4626.unwrap(0); + vm.stopPrank(); + } + + function test_getWrappedAmount() public view { + uint256 amount = 100 * 10 ** 18; + uint256 wrappedAmount = wHypERC4626.getWrappedAmount(amount); + + assertEq(wrappedAmount, underlyingToken.assetsToShares(amount)); + } + + function test_getUnderlyingAmount() public view { + uint256 amount = 100 * 10 ** 18; + uint256 underlyingAmount = wHypERC4626.getUnderlyingAmount(amount); + + assertEq(underlyingAmount, underlyingToken.sharesToAssets(amount)); + } + + function test_wrappedPerUnderlying() public view { + uint256 wrappedPerUnderlying = wHypERC4626.wrappedPerUnderlying(); + + assertEq( + wrappedPerUnderlying, + underlyingToken.assetsToShares(1 * 10 ** underlyingToken.decimals()) + ); + } + + function test_underlyingPerWrapped() public view { + uint256 underlyingPerWrapped = wHypERC4626.underlyingPerWrapped(); + + assertEq( + underlyingPerWrapped, + underlyingToken.sharesToAssets(1 * 10 ** underlyingToken.decimals()) + ); + } + + function test_decimals() public view { + uint8 decimals = wHypERC4626.decimals(); + + assertEq(decimals, underlyingToken.decimals()); + } +}