Skip to content

Commit

Permalink
Implement Connext xERC20 and Circle FiatToken collateral (#3618)
Browse files Browse the repository at this point in the history
### Description

- Implement XERC20 Collateral contract
- Implement FiatToken Collateral contract

### Drive-by

- Move vault collateral extension into token extensions folder

### Related issues

- Fixes #3486

### Backward compatibility

- Yes

### Testing

- Unit tests
  • Loading branch information
yorhodes authored May 6, 2024
1 parent 22f367f commit b6fdf2f
Show file tree
Hide file tree
Showing 16 changed files with 310 additions and 27 deletions.
7 changes: 7 additions & 0 deletions .changeset/bright-laws-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
'@hyperlane-xyz/core': minor
---

Implement XERC20 and FiatToken collateral warp routes
38 changes: 38 additions & 0 deletions solidity/contracts/test/ERC20Test.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ pragma solidity >=0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import "../token/interfaces/IXERC20.sol";
import "../token/interfaces/IFiatToken.sol";

contract ERC20Test is ERC20 {
uint8 public immutable _decimals;

Expand All @@ -28,3 +31,38 @@ contract ERC20Test is ERC20 {
_mint(account, amount);
}
}

contract FiatTokenTest is ERC20Test, IFiatToken {
constructor(
string memory name,
string memory symbol,
uint256 totalSupply,
uint8 __decimals
) ERC20Test(name, symbol, totalSupply, __decimals) {}

function burn(uint256 amount) public override {
_burn(msg.sender, amount);
}

function mint(address account, uint256 amount) public returns (bool) {
_mint(account, amount);
return true;
}
}

contract XERC20Test is ERC20Test, IXERC20 {
constructor(
string memory name,
string memory symbol,
uint256 totalSupply,
uint8 __decimals
) ERC20Test(name, symbol, totalSupply, __decimals) {}

function mint(address account, uint256 amount) public override {
_mint(account, amount);
}

function burn(address account, uint256 amount) public override {
_burn(account, amount);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {HypERC20Collateral} from "./HypERC20Collateral.sol";
import {HypERC20Collateral} from "../HypERC20Collateral.sol";

/**
* @title Hyperlane ERC20 Token Collateral with deposits collateral to a vault
Expand Down
32 changes: 32 additions & 0 deletions solidity/contracts/token/extensions/HypFiatTokenCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
pragma solidity >=0.8.0;

import {IFiatToken} from "../interfaces/IFiatToken.sol";
import {HypERC20Collateral} from "../HypERC20Collateral.sol";

// see https://github.com/circlefin/stablecoin-evm/blob/master/doc/tokendesign.md#issuing-and-destroying-tokens
contract HypFiatTokenCollateral is HypERC20Collateral {
constructor(
address _fiatToken,
address _mailbox
) HypERC20Collateral(_fiatToken, _mailbox) {}

function _transferFromSender(
uint256 _amount
) internal override returns (bytes memory metadata) {
// transfer amount to address(this)
metadata = super._transferFromSender(_amount);
// burn amount of address(this) balance
IFiatToken(address(wrappedToken)).burn(_amount);
}

function _transferTo(
address _recipient,
uint256 _amount,
bytes calldata /*metadata*/
) internal override {
require(
IFiatToken(address(wrappedToken)).mint(_recipient, _amount),
"FiatToken mint failed"
);
}
}
26 changes: 26 additions & 0 deletions solidity/contracts/token/extensions/HypXERC20Collateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
pragma solidity >=0.8.0;

import {IXERC20} from "../interfaces/IXERC20.sol";
import {HypERC20Collateral} from "../HypERC20Collateral.sol";

contract HypXERC20Collateral is HypERC20Collateral {
constructor(
address _xerc20,
address _mailbox
) HypERC20Collateral(_xerc20, _mailbox) {}

function _transferFromSender(
uint256 _amountOrId
) internal override returns (bytes memory metadata) {
IXERC20(address(wrappedToken)).burn(msg.sender, _amountOrId);
return "";
}

function _transferTo(
address _recipient,
uint256 _amountOrId,
bytes calldata /*metadata*/
) internal override {
IXERC20(address(wrappedToken)).mint(_recipient, _amountOrId);
}
}
24 changes: 24 additions & 0 deletions solidity/contracts/token/interfaces/IFiatToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;

// adapted from https://github.com/circlefin/stablecoin-evm
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IFiatToken is IERC20 {
/**
* @notice Allows a minter to burn some of its own tokens.
* @dev The caller must be a minter, must not be blacklisted, and the amount to burn
* should be less than or equal to the account's balance.
* @param _amount the amount of tokens to be burned.
*/
function burn(uint256 _amount) external;

/**
* @notice Mints fiat tokens to an address.
* @param _to The address that will receive the minted tokens.
* @param _amount The amount of tokens to mint. Must be less than or equal
* to the minterAllowance of the caller.
* @return True if the operation was successful.
*/
function mint(address _to, uint256 _amount) external returns (bool);
}
24 changes: 24 additions & 0 deletions solidity/contracts/token/interfaces/IXERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;

// adapted from https://github.com/defi-wonderland/xERC20

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IXERC20 is IERC20 {
/**
* @notice Mints tokens for a user
* @dev Can only be called by a minter
* @param _user The address of the user who needs tokens minted
* @param _amount The amount of tokens being minted
*/
function mint(address _user, uint256 _amount) external;

/**
* @notice Burns tokens for a user
* @dev Can only be called by a minter
* @param _user The address of the user who needs tokens burned
* @param _amount The amount of tokens being burned
*/
function burn(address _user, uint256 _amount) external;
}
108 changes: 107 additions & 1 deletion solidity/test/token/HypERC20.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transpa
import {Mailbox} from "../../contracts/Mailbox.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {ERC20Test} from "../../contracts/test/ERC20Test.sol";
import {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 {HypERC20} from "../../contracts/token/HypERC20.sol";
import {HypERC20Collateral} from "../../contracts/token/HypERC20Collateral.sol";
import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol";
import {IFiatToken} from "../../contracts/token/interfaces/IFiatToken.sol";
import {HypXERC20Collateral} from "../../contracts/token/extensions/HypXERC20Collateral.sol";
import {HypFiatTokenCollateral} from "../../contracts/token/extensions/HypFiatTokenCollateral.sol";
import {HypNative} from "../../contracts/token/HypNative.sol";
import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol";
import {TokenMessage} from "../../contracts/token/libs/TokenMessage.sol";
Expand Down Expand Up @@ -390,6 +394,108 @@ contract HypERC20CollateralTest is HypTokenTest {
}
}

contract HypXERC20CollateralTest is HypTokenTest {
using TypeCasts for address;
HypXERC20Collateral internal xerc20Collateral;

function setUp() public override {
super.setUp();

primaryToken = new XERC20Test(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS);

localToken = new HypXERC20Collateral(
address(primaryToken),
address(localMailbox)
);
xerc20Collateral = HypXERC20Collateral(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 HypFiatTokenCollateralTest is HypTokenTest {
using TypeCasts for address;
HypFiatTokenCollateral internal fiatTokenCollateral;

function setUp() public override {
super.setUp();

primaryToken = new FiatTokenTest(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS);

localToken = new HypFiatTokenCollateral(
address(primaryToken),
address(localMailbox)
);
fiatTokenCollateral = HypFiatTokenCollateral(address(localToken));

fiatTokenCollateral.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;
HypNative internal nativeToken;
Expand Down
2 changes: 1 addition & 1 deletion solidity/test/token/HypERC20CollateralVaultDeposit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {ERC4626Test} from "../../contracts/test/ERC4626/ERC4626Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {HypTokenTest} from "./HypERC20.t.sol";

import {HypERC20CollateralVaultDeposit} from "../../contracts/token/HypERC20CollateralVaultDeposit.sol";
import {HypERC20CollateralVaultDeposit} from "../../contracts/token/extensions/HypERC20CollateralVaultDeposit.sol";
import "../../contracts/test/ERC4626/ERC4626Test.sol";

contract HypERC20CollateralVaultDepositTest is HypTokenTest {
Expand Down
6 changes: 2 additions & 4 deletions typescript/cli/examples/warp-route-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
# native
# collateral
# synthetic
# collateralUri
# syntheticUri
# fastCollateral
# fastSynthetic
#
# see comprehensive [list](https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/token/config.ts#L8)
---
anvil1:
type: native
Expand Down
18 changes: 18 additions & 0 deletions typescript/sdk/src/token/Token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ const STANDARD_TO_TOKEN: Record<TokenStandard, TokenArgs | null> = {
symbol: 'USDC',
name: 'USDC',
},
[TokenStandard.EvmHypXERC20Collateral]: {
chainName: TestChainName.test3,
standard: TokenStandard.EvmHypXERC20Collateral,
addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131',
collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930',
decimals: 18,
symbol: 'USDC',
name: 'USDC',
},
[TokenStandard.EvmHypFiatCollateral]: {
chainName: TestChainName.test3,
standard: TokenStandard.EvmHypXERC20Collateral,
addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131',
collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930',
decimals: 18,
symbol: 'USDC',
name: 'USDC',
},
[TokenStandard.EvmHypCollateralVault]: {
chainName: TestChainName.test3,
standard: TokenStandard.EvmHypCollateral,
Expand Down
7 changes: 6 additions & 1 deletion typescript/sdk/src/token/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,12 @@ export class Token implements IToken {
return new EvmHypNativeAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
} else if (standard === TokenStandard.EvmHypCollateral) {
} else if (
standard === TokenStandard.EvmHypCollateral ||
standard === TokenStandard.EvmHypCollateralVault ||
standard === TokenStandard.EvmHypXERC20Collateral ||
standard === TokenStandard.EvmHypFiatCollateral
) {
return new EvmHypCollateralAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
Expand Down
Loading

0 comments on commit b6fdf2f

Please sign in to comment.