Prevent warp transfers to zero-ish addresses (#3426)

### Description

- Prevent warp transfers to zero-ish addresses
- Increase address utility test coverage

### Related issues

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

### Backward compatibility

Yes

### Testing

Tested in warp UI and added unit tests
pull/3427/head
J M Rossy 8 months ago committed by GitHub
parent a72c3cf1a5
commit 5daaae274d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/loud-falcons-hang.md
  2. 28
      typescript/sdk/src/warp/WarpCore.test.ts
  3. 6
      typescript/sdk/src/warp/WarpCore.ts
  4. 63
      typescript/utils/src/addresses.test.ts
  5. 20
      typescript/utils/src/addresses.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/utils': patch
'@hyperlane-xyz/sdk': patch
---
Prevent warp transfers to zero-ish addresses

@ -1,5 +1,4 @@
import { expect } from 'chai';
import { ethers } from 'ethers';
import fs from 'fs';
import path from 'path';
import sinon from 'sinon';
@ -22,6 +21,7 @@ const MOCK_INTERCHAIN_QUOTE = { amount: 20_000n };
const TRANSFER_AMOUNT = BigInt('1000000000000000000'); // 1 units @ 18 decimals
const BIG_TRANSFER_AMOUNT = BigInt('100000000000000000000'); // 100 units @ 18 decimals
const MOCK_BALANCE = BigInt('10000000000000000000'); // 10 units @ 18 decimals
const MOCK_ADDRESS = '0x0000000000000000000000000000000000000001';
describe('WarpCore', () => {
const multiProvider = new MultiProtocolProvider();
@ -94,7 +94,7 @@ describe('WarpCore', () => {
const result = await warpCore.estimateTransferRemoteFees({
originToken: token,
destination,
sender: ethers.constants.AddressZero,
sender: MOCK_ADDRESS,
});
expect(
result.localQuote.token.standard,
@ -193,40 +193,40 @@ describe('WarpCore', () => {
const validResult = await warpCore.validateTransfer({
originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT),
destination: Chains.arbitrum,
recipient: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
recipient: MOCK_ADDRESS,
sender: MOCK_ADDRESS,
});
expect(validResult).to.be.null;
const invalidChain = await warpCore.validateTransfer({
originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT),
destination: 'fakechain',
recipient: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
recipient: MOCK_ADDRESS,
sender: MOCK_ADDRESS,
});
expect(Object.keys(invalidChain || {})[0]).to.equal('destination');
const invalidRecipient = await warpCore.validateTransfer({
originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT),
destination: Chains.neutron,
recipient: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
recipient: MOCK_ADDRESS,
sender: MOCK_ADDRESS,
});
expect(Object.keys(invalidRecipient || {})[0]).to.equal('recipient');
const invalidAmount = await warpCore.validateTransfer({
originTokenAmount: evmHypNative.amount(-10),
destination: Chains.arbitrum,
recipient: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
recipient: MOCK_ADDRESS,
sender: MOCK_ADDRESS,
});
expect(Object.keys(invalidAmount || {})[0]).to.equal('amount');
const insufficientBalance = await warpCore.validateTransfer({
originTokenAmount: evmHypNative.amount(BIG_TRANSFER_AMOUNT),
destination: Chains.arbitrum,
recipient: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
recipient: MOCK_ADDRESS,
sender: MOCK_ADDRESS,
});
expect(Object.keys(insufficientBalance || {})[0]).to.equal('amount');
@ -254,8 +254,8 @@ describe('WarpCore', () => {
const result = await warpCore.getTransferRemoteTxs({
originTokenAmount: token.amount(TRANSFER_AMOUNT),
destination,
sender: ethers.constants.AddressZero,
recipient: ethers.constants.AddressZero,
sender: MOCK_ADDRESS,
recipient: MOCK_ADDRESS,
});
expect(result.length).to.equal(1);
expect(

@ -8,6 +8,7 @@ import {
convertDecimals,
convertToProtocolAddress,
isValidAddress,
isZeroishAddress,
} from '@hyperlane-xyz/utils';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
@ -545,8 +546,9 @@ export class WarpCore {
this.multiProvider.getChainMetadata(destination);
const { protocol, bech32Prefix } = destinationMetadata;
// Ensure recip address is valid for the destination chain's protocol
if (!isValidAddress(recipient, protocol))
if (!isValidAddress(recipient, protocol) || isZeroishAddress(recipient))
return { recipient: 'Invalid recipient' };
// Also ensure the address denom is correct if the dest protocol is Cosmos
if (protocol === ProtocolType.Cosmos) {
if (!bech32Prefix) {
@ -554,7 +556,7 @@ export class WarpCore {
return { destination: 'Invalid chain data' };
} else if (!recipient.startsWith(bech32Prefix)) {
this.logger(`Recipient prefix should be ${bech32Prefix}`);
return { recipient: `Invalid recipient prefix` };
return { recipient: 'Invalid recipient prefix' };
}
}
return null;

@ -0,0 +1,63 @@
import { expect } from 'chai';
import {
addressToBytes,
bytesToProtocolAddress,
isZeroishAddress,
} from './addresses';
import { ProtocolType } from './types';
const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
const ETH_NON_ZERO_ADDR = '0x0000000000000000000000000000000000000001';
const COS_ZERO_ADDR = 'cosmos1000';
const COS_NON_ZERO_ADDR =
'neutron1jyyjd3x0jhgswgm6nnctxvzla8ypx50tew3ayxxwkrjfxhvje6kqzvzudq';
const SOL_ZERO_ADDR = '111111';
const SOL_NON_ZERO_ADDR = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
// TODO increase address utility test coverage
describe('Address utilities', () => {
describe('isZeroishAddress', () => {
it('Identifies 0-ish addresses', () => {
expect(isZeroishAddress('0x')).to.be.true;
expect(isZeroishAddress(ETH_ZERO_ADDR)).to.be.true;
expect(isZeroishAddress(COS_ZERO_ADDR)).to.be.true;
expect(isZeroishAddress(SOL_ZERO_ADDR)).to.be.true;
});
it('Identifies non-0-ish addresses', () => {
expect(isZeroishAddress(ETH_NON_ZERO_ADDR)).to.be.false;
expect(isZeroishAddress(COS_NON_ZERO_ADDR)).to.be.false;
expect(isZeroishAddress(SOL_NON_ZERO_ADDR)).to.be.false;
});
});
describe('addressToBytes', () => {
it('Converts addresses to bytes', () => {
expect(addressToBytes(ETH_NON_ZERO_ADDR).length).to.equal(32);
});
it('Rejects zeroish addresses', () => {
expect(() => addressToBytes(ETH_ZERO_ADDR)).to.throw(Error);
expect(() => addressToBytes(COS_ZERO_ADDR)).to.throw(Error);
expect(() => addressToBytes(SOL_ZERO_ADDR)).to.throw(Error);
});
});
describe('bytesToProtocolAddress', () => {
it('Converts bytes to address', () => {
expect(
bytesToProtocolAddress(
addressToBytes(ETH_NON_ZERO_ADDR),
ProtocolType.Ethereum,
),
).to.equal(ETH_NON_ZERO_ADDR);
});
it('Rejects zeroish addresses', () => {
expect(() =>
bytesToProtocolAddress(
new Uint8Array([0, 0, 0]),
ProtocolType.Ethereum,
),
).to.throw(Error);
});
});
});

@ -4,6 +4,7 @@ import { utils as ethersUtils } from 'ethers';
import { isNullish } from './typeof';
import { Address, HexString, ProtocolType } from './types';
import { assert } from './validation';
const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
const SEALEVEL_ADDRESS_REGEX = /^[a-zA-Z0-9]{36,44}$/;
@ -24,8 +25,9 @@ const EVM_TX_HASH_REGEX = /^0x([A-Fa-f0-9]{64})$/;
const SEALEVEL_TX_HASH_REGEX = /^[a-zA-Z1-9]{88}$/;
const COSMOS_TX_HASH_REGEX = /^(0x)?[A-Fa-f0-9]{64}$/;
const ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/;
const COSMOS_ZEROISH_ADDRESS_REGEX = /^[a-z]{1,10}?1[0]{38}$/;
const EVM_ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/;
const SEALEVEL_ZEROISH_ADDRESS_REGEX = /^1+$/;
const COSMOS_ZEROISH_ADDRESS_REGEX = /^[a-z]{1,10}?1[0]+$/;
export function isAddressEvm(address: Address) {
return EVM_ADDRESS_REGEX.test(address);
@ -214,7 +216,8 @@ export function isValidTransactionHash(input: string, protocol: ProtocolType) {
export function isZeroishAddress(address: Address) {
return (
ZEROISH_ADDRESS_REGEX.test(address) ||
EVM_ZEROISH_ADDRESS_REGEX.test(address) ||
SEALEVEL_ZEROISH_ADDRESS_REGEX.test(address) ||
COSMOS_ZEROISH_ADDRESS_REGEX.test(address)
);
}
@ -264,7 +267,7 @@ export function addressToBytes(
address: Address,
protocol?: ProtocolType,
): Uint8Array {
return routeAddressUtil(
const bytes = routeAddressUtil(
{
[ProtocolType.Ethereum]: addressToBytesEvm,
[ProtocolType.Sealevel]: addressToBytesSol,
@ -274,6 +277,11 @@ export function addressToBytes(
new Uint8Array(),
protocol,
);
assert(
bytes.length && !bytes.every((b) => b == 0),
'address bytes must not be empty',
);
return bytes;
}
export function addressToByteHexString(
@ -329,6 +337,10 @@ export function bytesToProtocolAddress(
toProtocol: ProtocolType,
prefix?: string,
) {
assert(
bytes.length && !bytes.every((b) => b == 0),
'address bytes must not be empty',
);
if (toProtocol === ProtocolType.Ethereum) {
return bytesToAddressEvm(bytes);
} else if (toProtocol === ProtocolType.Sealevel) {

Loading…
Cancel
Save