Skip to content

Commit

Permalink
Handle createProxyWithNonce relay limiting (#1115)
Browse files Browse the repository at this point in the history
This adds relay limiting for `createProxyWithNonce` calls. It verifies the calldata and extracts the proposed owners.

---

* Handle `multiSend` relay limiting

* Add comment

* Add an leverage `isCall` method to `AbiDecoder`

* Handle `createProxyWithNonce` relay limiting

* Simplify helpers and remove comments

* Fix lint

* Add return type

* Remove duplicate decoding
  • Loading branch information
iamacook authored Feb 13, 2024
1 parent 3555662 commit b4eb0f6
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 5 deletions.
51 changes: 51 additions & 0 deletions src/domain/alerts/__tests__/safe-transactions.encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,57 @@ function getPrevOwner(ownerToRemove: Hex, owners?: Safe['owners']): Hex {
: getAddress(owners[ownerIndex - 1]);
}

// setup

type SetupArgs = {
owners: Hex[];
threshold: bigint;
to: Hex;
data: Hex;
fallbackHandler: Hex;
paymentToken: Hex;
payment: bigint;
paymentReceiver: Hex;
};

class SetupEncoder<T extends SetupArgs> extends Builder<T> implements IEncoder {
static readonly FUNCTION_SIGNATURE =
'function setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address paymentReceiver)';

encode(): Hex {
const abi = parseAbi([SetupEncoder.FUNCTION_SIGNATURE]);

const args = this.build();

return encodeFunctionData({
abi,
functionName: 'setup',
args: [
args.owners,
args.threshold,
args.to,
args.data,
args.fallbackHandler,
args.paymentToken,
args.payment,
args.paymentReceiver,
],
});
}
}

export function setupEncoder(): SetupEncoder<SetupArgs> {
return new SetupEncoder()
.with('owners', [getAddress(faker.finance.ethereumAddress())])
.with('threshold', BigInt(1))
.with('to', getAddress(faker.finance.ethereumAddress()))
.with('data', '0x')
.with('fallbackHandler', ZERO_ADDRESS)
.with('paymentToken', ZERO_ADDRESS)
.with('payment', BigInt(0))
.with('paymentReceiver', ZERO_ADDRESS);
}

// execTransaction

type ExecTransactionArgs = {
Expand Down
21 changes: 21 additions & 0 deletions src/domain/alerts/contracts/safe-decoder.helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
changeThresholdEncoder,
execTransactionEncoder,
removeOwnerEncoder,
setupEncoder,
swapOwnerEncoder,
} from '@/domain/alerts/__tests__/safe-transactions.encoder';

Expand All @@ -17,6 +18,26 @@ describe('SafeDecoder', () => {
target = new SafeDecoder();
});

it('decodes a setup function call correctly', () => {
const setup = setupEncoder();
const args = setup.build();
const data = setup.encode();

expect(target.decodeFunctionData({ data })).toEqual({
functionName: 'setup',
args: [
args.owners,
args.threshold,
args.to,
args.data,
args.fallbackHandler,
args.paymentToken,
args.payment,
args.paymentReceiver,
],
});
});

it('decodes an addOwnerWithThreshold function call correctly', () => {
const addOwnerWithThreshold = addOwnerWithThresholdEncoder();
const args = addOwnerWithThreshold.build();
Expand Down
8 changes: 5 additions & 3 deletions src/domain/alerts/contracts/safe-decoder.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Injectable } from '@nestjs/common';
import { parseAbi } from 'viem';
import { AbiDecoder } from '@/domain/alerts/contracts/abi-decoder.helper';

const OWNER_MANAGER_ABI = parseAbi([
const SAFE_ABI = parseAbi([
'function setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address paymentReceiver)',
// Owner management
'function addOwnerWithThreshold(address owner, uint256 _threshold)',
'function removeOwner(address prevOwner, address owner, uint256 _threshold)',
'function swapOwner(address prevOwner, address oldOwner, address newOwner)',
Expand All @@ -11,8 +13,8 @@ const OWNER_MANAGER_ABI = parseAbi([
]);

@Injectable()
export class SafeDecoder extends AbiDecoder<typeof OWNER_MANAGER_ABI> {
export class SafeDecoder extends AbiDecoder<typeof SAFE_ABI> {
constructor() {
super(OWNER_MANAGER_ABI);
super(SAFE_ABI);
}
}
14 changes: 14 additions & 0 deletions src/domain/relay/contracts/proxy-factory-decoder.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { parseAbi } from 'viem';
import { AbiDecoder } from '@/domain/alerts/contracts/abi-decoder.helper';

const PROXY_FACTORY_ABI = parseAbi([
'function createProxyWithNonce(address _singleton, bytes memory initializer, uint256 saltNonce)',
]);

@Injectable()
export class ProxyFactoryDecoder extends AbiDecoder<typeof PROXY_FACTORY_ABI> {
constructor() {
super(PROXY_FACTORY_ABI);
}
}
74 changes: 72 additions & 2 deletions src/domain/relay/limit-addresses.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { Erc20ContractHelper } from '@/domain/relay/contracts/erc20-contract.hel
import { SafeContractHelper } from '@/domain/relay/contracts/safe-contract.helper';
import { ILoggingService, LoggingService } from '@/logging/logging.interface';
import { MultiSendDecoder } from '@/domain/alerts/contracts/multi-send-decoder.helper';
import { ProxyFactoryDecoder } from '@/domain/relay/contracts/proxy-factory-decoder.helper';
import {
getSafeSingletonDeployment,
getSafeL2SingletonDeployment,
} from '@safe-global/safe-deployments';
import { SafeDecoder } from '@/domain/alerts/contracts/safe-decoder.helper';

export interface RelayPayload {
chainId: string;
Expand All @@ -14,14 +20,19 @@ export interface RelayPayload {

@Injectable()
export class LimitAddressesMapper {
// TODO: Abstract with that from SafeContractHelper
private static SUPPORTED_SAFE_VERSION = '1.3.0';

constructor(
@Inject(LoggingService) private readonly loggingService: ILoggingService,
private readonly safeContract: SafeContractHelper,
private readonly erc20Contract: Erc20ContractHelper,
private readonly safeDecoder: SafeDecoder,
private readonly multiSendDecoder: MultiSendDecoder,
private readonly proxyFactoryDecoder: ProxyFactoryDecoder,
) {}

getLimitAddresses(relayPayload: RelayPayload): Hex[] {
getLimitAddresses(relayPayload: RelayPayload): readonly Hex[] {
if (this.isValidExecTransactionCall(relayPayload.to, relayPayload.data)) {
return [relayPayload.to];
}
Expand All @@ -32,7 +43,11 @@ export class LimitAddressesMapper {
return [safeAddress];
}

// TODO Handle create proxy with nonce
if (
this.isValidCreateProxyWithNonce(relayPayload.chainId, relayPayload.data)
) {
return this.getOwnersFromCreateProxyWithNonce(relayPayload.data);
}

throw Error('Cannot get limit addresses – Invalid transfer');
}
Expand Down Expand Up @@ -101,4 +116,59 @@ export class LimitAddressesMapper {

return firstRecipient;
};

private isValidCreateProxyWithNonce(chainId: string, data: Hex): boolean {
let singleton: string | null = null;

try {
const decoded = this.proxyFactoryDecoder.decodeFunctionData({
data,
});

if (decoded.functionName !== 'createProxyWithNonce') {
return false;
}

singleton = decoded.args[0];
} catch (e) {
return false;
}

const safeL1Deployment = getSafeSingletonDeployment({
version: LimitAddressesMapper.SUPPORTED_SAFE_VERSION,
network: chainId,
});
const safeL2Deployment = getSafeL2SingletonDeployment({
version: LimitAddressesMapper.SUPPORTED_SAFE_VERSION,
network: chainId,
});

const isL1Singleton =
safeL1Deployment?.networkAddresses[chainId] === singleton;
const isL2Singleton =
safeL2Deployment?.networkAddresses[chainId] === singleton;

return isL1Singleton || isL2Singleton;
}

private getOwnersFromCreateProxyWithNonce(data: Hex): readonly Hex[] {
const decodedProxyFactory = this.proxyFactoryDecoder.decodeFunctionData({
data,
});

if (decodedProxyFactory.functionName !== 'createProxyWithNonce') {
throw Error('Not a createProxyWithNonce call');
}

const initializer = decodedProxyFactory.args[1];
const decodedSafe = this.safeDecoder.decodeFunctionData({
data: initializer,
});

if (decodedSafe.functionName !== 'setup') {
throw Error('Not a setup call');
}

return decodedSafe.args[0];
}
}
2 changes: 2 additions & 0 deletions src/domain/relay/relay.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { LimitAddressesMapper } from '@/domain/relay/limit-addresses.mapper';
import { Erc20ContractHelper } from '@/domain/relay/contracts/erc20-contract.helper';
import { SafeContractHelper } from '@/domain/relay/contracts/safe-contract.helper';
import { MultiSendDecoder } from '@/domain/alerts/contracts/multi-send-decoder.helper';
import { ProxyFactoryDecoder } from '@/domain/relay/contracts/proxy-factory-decoder.helper';

@Module({
providers: [
Expand All @@ -12,6 +13,7 @@ import { MultiSendDecoder } from '@/domain/alerts/contracts/multi-send-decoder.h
SafeContractHelper,
// TODO: Generify AlertsDecodersModule and import here
MultiSendDecoder,
ProxyFactoryDecoder,
],
exports: [LimitAddressesMapper],
})
Expand Down

0 comments on commit b4eb0f6

Please sign in to comment.