Implement Connext xERC20 and Circle FiatToken collateral (#3618)

### Description

- Implement XERC20 Collateral contract
- Implement FiatToken Collateral contract

### Drive-by

- Move vault collateral extension into token extensions folder

### Related issues

- Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3486

### Backward compatibility

- Yes

### Testing

- Unit tests
pull/3533/head
Yorke Rhodes 6 months ago committed by GitHub
parent 22f367ff6c
commit b6fdf2f7fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/bright-laws-draw.md
  2. 38
      solidity/contracts/test/ERC20Test.sol
  3. 2
      solidity/contracts/token/extensions/HypERC20CollateralVaultDeposit.sol
  4. 32
      solidity/contracts/token/extensions/HypFiatTokenCollateral.sol
  5. 26
      solidity/contracts/token/extensions/HypXERC20Collateral.sol
  6. 24
      solidity/contracts/token/interfaces/IFiatToken.sol
  7. 24
      solidity/contracts/token/interfaces/IXERC20.sol
  8. 108
      solidity/test/token/HypERC20.t.sol
  9. 2
      solidity/test/token/HypERC20CollateralVaultDeposit.t.sol
  10. 6
      typescript/cli/examples/warp-route-deployment.yaml
  11. 18
      typescript/sdk/src/token/Token.test.ts
  12. 7
      typescript/sdk/src/token/Token.ts
  13. 10
      typescript/sdk/src/token/TokenStandard.ts
  14. 6
      typescript/sdk/src/token/config.ts
  15. 6
      typescript/sdk/src/token/contracts.ts
  16. 21
      typescript/sdk/src/token/deploy.ts

@ -0,0 +1,7 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
'@hyperlane-xyz/core': minor
---
Implement XERC20 and FiatToken collateral warp routes

@ -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;
@ -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);
}
}

@ -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

@ -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"
);
}
}

@ -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);
}
}

@ -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);
}

@ -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;
}

@ -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";
@ -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;

@ -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 {

@ -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

@ -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,

@ -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,
});

@ -14,6 +14,8 @@ export enum TokenStandard {
EvmNative = 'EvmNative',
EvmHypNative = 'EvmHypNative',
EvmHypCollateral = 'EvmHypCollateral',
EvmHypXERC20Collateral = 'EvmHypXERC20Collateral',
EvmHypFiatCollateral = 'EvmHypFiatCollateral',
EvmHypCollateralVault = 'EvmHypCollateralVault',
EvmHypSynthetic = 'EvmHypSynthetic',
@ -50,6 +52,8 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record<TokenStandard, ProtocolType> = {
EvmHypCollateral: ProtocolType.Ethereum,
EvmHypCollateralVault: ProtocolType.Ethereum,
EvmHypSynthetic: ProtocolType.Ethereum,
EvmHypXERC20Collateral: ProtocolType.Ethereum,
EvmHypFiatCollateral: ProtocolType.Ethereum,
// Sealevel (Solana)
SealevelSpl: ProtocolType.Sealevel,
@ -92,6 +96,8 @@ export const TOKEN_NFT_STANDARDS = [
export const TOKEN_COLLATERALIZED_STANDARDS = [
TokenStandard.EvmHypCollateral,
TokenStandard.EvmHypNative,
TokenStandard.EvmHypXERC20Collateral,
TokenStandard.EvmHypFiatCollateral,
TokenStandard.SealevelHypCollateral,
TokenStandard.SealevelHypNative,
TokenStandard.CwHypCollateral,
@ -101,6 +107,8 @@ export const TOKEN_COLLATERALIZED_STANDARDS = [
export const TOKEN_HYP_STANDARDS = [
TokenStandard.EvmHypNative,
TokenStandard.EvmHypCollateral,
TokenStandard.EvmHypXERC20Collateral,
TokenStandard.EvmHypFiatCollateral,
TokenStandard.EvmHypSynthetic,
TokenStandard.SealevelHypNative,
TokenStandard.SealevelHypCollateral,
@ -129,6 +137,8 @@ export const TOKEN_COSMWASM_STANDARDS = [
export const TOKEN_TYPE_TO_STANDARD: Record<TokenType, TokenStandard> = {
[TokenType.native]: TokenStandard.EvmHypNative,
[TokenType.collateral]: TokenStandard.EvmHypCollateral,
[TokenType.collateralFiat]: TokenStandard.EvmHypFiatCollateral,
[TokenType.collateralXERC20]: TokenStandard.EvmHypXERC20Collateral,
[TokenType.collateralVault]: TokenStandard.EvmHypCollateralVault,
[TokenType.collateralUri]: TokenStandard.EvmHypCollateral,
[TokenType.fastCollateral]: TokenStandard.EvmHypCollateral,

@ -11,6 +11,8 @@ export enum TokenType {
syntheticUri = 'syntheticUri',
collateral = 'collateral',
collateralVault = 'collateralVault',
collateralXERC20 = 'collateralXERC20',
collateralFiat = 'collateralFiat',
fastCollateral = 'fastCollateral',
collateralUri = 'collateralUri',
native = 'native',
@ -41,6 +43,8 @@ export type SyntheticConfig = z.infer<typeof SyntheticConfigSchema>;
export type CollateralConfig = {
type:
| TokenType.collateral
| TokenType.collateralXERC20
| TokenType.collateralFiat
| TokenType.collateralUri
| TokenType.fastCollateral
| TokenType.fastSynthetic
@ -57,6 +61,8 @@ export const isCollateralConfig = (
config: TokenConfig,
): config is CollateralConfig =>
config.type === TokenType.collateral ||
config.type === TokenType.collateralXERC20 ||
config.type === TokenType.collateralFiat ||
config.type === TokenType.collateralUri ||
config.type === TokenType.fastCollateral ||
config.type == TokenType.collateralVault;

@ -8,8 +8,10 @@ import {
HypERC721URICollateral__factory,
HypERC721URIStorage__factory,
HypERC721__factory,
HypFiatTokenCollateral__factory,
HypNativeScaled__factory,
HypNative__factory,
HypXERC20Collateral__factory,
} from '@hyperlane-xyz/core';
import { proxiedFactories } from '../router/types.js';
@ -21,6 +23,8 @@ export const hypERC20contracts = {
[TokenType.fastSynthetic]: 'FastHypERC20',
[TokenType.synthetic]: 'HypERC20',
[TokenType.collateral]: 'HypERC20Collateral',
[TokenType.collateralFiat]: 'HypFiatTokenCollateral',
[TokenType.collateralXERC20]: 'HypXERC20Collateral',
[TokenType.collateralVault]: 'HypERC20CollateralVaultDeposit',
[TokenType.native]: 'HypNative',
[TokenType.nativeScaled]: 'HypNativeScaled',
@ -33,6 +37,8 @@ export const hypERC20Tokenfactories = {
[TokenType.synthetic]: new HypERC20__factory(),
[TokenType.collateral]: new HypERC20Collateral__factory(),
[TokenType.collateralVault]: new HypERC20CollateralVaultDeposit__factory(),
[TokenType.collateralFiat]: new HypFiatTokenCollateral__factory(),
[TokenType.collateralXERC20]: new HypXERC20Collateral__factory(),
[TokenType.native]: new HypNative__factory(),
[TokenType.nativeScaled]: new HypNativeScaled__factory(),
};

@ -28,9 +28,7 @@ import {
TokenMetadata,
TokenType,
isCollateralConfig,
isCollateralVaultConfig,
isErc20Metadata,
isFastConfig,
isNativeConfig,
isSyntheticConfig,
isTokenMetadata,
@ -38,6 +36,7 @@ import {
} from './config.js';
import {
HypERC20Factories,
HypERC20contracts,
HypERC721Factories,
HypERC721contracts,
hypERC20contracts,
@ -67,23 +66,7 @@ export class HypERC20Deployer extends GasRouterDeployer<
}
routerContractKey(config: ERC20RouterConfig) {
if (isCollateralConfig(config)) {
if (isFastConfig(config)) {
return TokenType.fastCollateral;
} else if (isCollateralVaultConfig(config)) {
return TokenType.collateralVault;
} else {
return TokenType.collateral;
}
} else if (isNativeConfig(config)) {
return config.scale ? TokenType.nativeScaled : TokenType.native;
} else if (isSyntheticConfig(config)) {
return isFastConfig(config)
? TokenType.fastSynthetic
: TokenType.synthetic;
} else {
throw new Error('Unknown collateral type when constructing router name');
}
return config.type as keyof HypERC20contracts;
}
async constructorArgs<K extends keyof HypERC20Factories>(

Loading…
Cancel
Save