@rhinestone/shared-configs 2.36.2 → 2.36.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/configs/chain-indexer.json +2 -2
- package/dist/configs/contracts.json +2 -2
- package/dist/scripts/__tests__/app-fees.test.d.ts +1 -0
- package/dist/scripts/__tests__/app-fees.test.js +156 -0
- package/dist/scripts/app-fees.d.ts +52 -0
- package/dist/scripts/app-fees.js +145 -0
- package/dist/scripts/check-app-fees.d.ts +1 -0
- package/dist/scripts/check-app-fees.js +92 -0
- package/dist/scripts/generate.js +7 -0
- package/dist/src/generated/contracts.js +2 -2
- package/dist/src/generated/packageVersion.d.ts +1 -1
- package/dist/src/generated/packageVersion.js +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ Source-of-truth lives in Yeet Jsonnet config:
|
|
|
18
18
|
2. Regenerate configs with `bun run generate` from this directory (requires Go)
|
|
19
19
|
3. Merge to `main` to publish `@rhinestone/shared-configs`
|
|
20
20
|
|
|
21
|
+
App-fee custody lives in `config/app-fees.json`. Generation verifies its signed
|
|
22
|
+
ownership proofs and version-pinned SDK account derivations. See
|
|
23
|
+
[`docs/app-fees.md`](../docs/app-fees.md) for verification and rollout commands.
|
|
24
|
+
|
|
21
25
|
## Token authorizations
|
|
22
26
|
|
|
23
27
|
An EVM token with verified token-native signed authorization support carries an
|
|
@@ -394,7 +394,7 @@
|
|
|
394
394
|
"max_catchup_signatures": 5000,
|
|
395
395
|
"overlap_slots": 300,
|
|
396
396
|
"page_limit": 1000,
|
|
397
|
-
"poll_interval_ms":
|
|
397
|
+
"poll_interval_ms": 10000,
|
|
398
398
|
"topics": [
|
|
399
399
|
"5q44zFF8gziimbCipqNQ7GYed8pkTRDmUKFUFTwd4DjB"
|
|
400
400
|
],
|
|
@@ -405,7 +405,7 @@
|
|
|
405
405
|
"max_catchup_signatures": 5000,
|
|
406
406
|
"overlap_slots": 300,
|
|
407
407
|
"page_limit": 1000,
|
|
408
|
-
"poll_interval_ms":
|
|
408
|
+
"poll_interval_ms": 5000,
|
|
409
409
|
"topics": [
|
|
410
410
|
"5q44zFF8gziimbCipqNQ7GYed8pkTRDmUKFUFTwd4DjB"
|
|
411
411
|
],
|
|
@@ -242,8 +242,8 @@
|
|
|
242
242
|
"compact": "0x00000000000000171ede64904551eeDF3C6C9788",
|
|
243
243
|
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
244
244
|
"feeCollector": "0xfeebabe17996b3346cf80f04a1f072102a90cf09",
|
|
245
|
-
"appFeeCollector": "
|
|
246
|
-
"appFeePot": "
|
|
245
|
+
"appFeeCollector": "0x86DB034A82072141D286e4e8dfcE653B75b3B5bd",
|
|
246
|
+
"appFeePot": "0x2Aaac3FEd8b9A1e7114620D395ACF604772eF2B0",
|
|
247
247
|
"tokenTransferProxy": "0x5afce415b4370e5efd8b9be784d21c331beab965"
|
|
248
248
|
}
|
|
249
249
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const bun_test_1 = require("bun:test");
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const secp256k1_1 = require("@noble/curves/secp256k1");
|
|
11
|
+
const viem_1 = require("viem");
|
|
12
|
+
const app_fees_1 = require("../app-fees");
|
|
13
|
+
const root = node_path_1.default.resolve(__dirname, '../../..');
|
|
14
|
+
const profiles = (0, app_fees_1.loadAppFeeProfiles)(root);
|
|
15
|
+
const emptyHashAddress = '0xdcc703c0E500B653Ca82273B7BFAd8045D85a470';
|
|
16
|
+
function ownershipProof(env) {
|
|
17
|
+
return JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(root, `docs/app-fee-collector.${env}.json`), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
(0, bun_test_1.describe)('KMS public-key derivation', () => {
|
|
20
|
+
// Source: SEC 2 secp256k1 generator point, encoded as DER SPKI.
|
|
21
|
+
const publicKey = Buffer.from('3056301006072a8648ce3d020106052b8104000a03420004' +
|
|
22
|
+
'79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' +
|
|
23
|
+
'483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', 'hex');
|
|
24
|
+
(0, bun_test_1.test)("derives the standard generator's Ethereum address", () => {
|
|
25
|
+
(0, bun_test_1.expect)((0, app_fees_1.collectorFromKmsPublicKey)({
|
|
26
|
+
KeySpec: 'ECC_SECG_P256K1',
|
|
27
|
+
PublicKey: publicKey.toString('base64'),
|
|
28
|
+
})).toBe('0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf');
|
|
29
|
+
});
|
|
30
|
+
(0, bun_test_1.test)('refuses empty and truncated KMS responses instead of hashing empty bytes', () => {
|
|
31
|
+
for (const value of [
|
|
32
|
+
undefined,
|
|
33
|
+
'',
|
|
34
|
+
publicKey.subarray(0, 30).toString('base64'),
|
|
35
|
+
]) {
|
|
36
|
+
(0, bun_test_1.expect)(() => (0, app_fees_1.collectorFromKmsPublicKey)({
|
|
37
|
+
KeySpec: 'ECC_SECG_P256K1',
|
|
38
|
+
PublicKey: value,
|
|
39
|
+
})).toThrow();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
(0, bun_test_1.test)('refuses a different elliptic curve even when it has 32-byte coordinates', () => {
|
|
43
|
+
const { publicKey: p256 } = (0, node_crypto_1.generateKeyPairSync)('ec', {
|
|
44
|
+
namedCurve: 'prime256v1',
|
|
45
|
+
});
|
|
46
|
+
(0, bun_test_1.expect)(() => (0, app_fees_1.collectorFromKmsPublicKey)({
|
|
47
|
+
KeySpec: 'ECC_SECG_P256K1',
|
|
48
|
+
PublicKey: p256
|
|
49
|
+
.export({ format: 'der', type: 'spki' })
|
|
50
|
+
.toString('base64'),
|
|
51
|
+
})).toThrow('secp256k1');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
(0, bun_test_1.describe)('app-fee custody profiles', () => {
|
|
55
|
+
for (const env of ['prod', 'staging']) {
|
|
56
|
+
(0, bun_test_1.test)(`${env} factory calldata installs its configured KMS owner`, async () => {
|
|
57
|
+
const profile = profiles[env];
|
|
58
|
+
const account = await (0, app_fees_1.deriveAppFeeAccount)(profile);
|
|
59
|
+
const factory = (0, viem_1.decodeFunctionData)({
|
|
60
|
+
abi: (0, viem_1.parseAbi)(['function createAccount(bytes,bytes32)']),
|
|
61
|
+
data: account.factoryData,
|
|
62
|
+
});
|
|
63
|
+
const [, bootstrapData] = (0, viem_1.decodeAbiParameters)([{ type: 'address' }, { type: 'bytes' }], factory.args[0]);
|
|
64
|
+
const bootstrap = (0, viem_1.decodeFunctionData)({
|
|
65
|
+
abi: (0, viem_1.parseAbi)([
|
|
66
|
+
'struct BootstrapConfig {address module;bytes initData;}',
|
|
67
|
+
'struct BootstrapPreValidationHookConfig {uint256 hookType;address module;bytes data;}',
|
|
68
|
+
'function initNexusWithDefaultValidatorAndOtherModulesNoRegistry(bytes defaultValidatorInitData,BootstrapConfig[] validators,BootstrapConfig[] executors,BootstrapConfig hook,BootstrapConfig[] fallbacks,BootstrapPreValidationHookConfig[] preValidationHooks)',
|
|
69
|
+
]),
|
|
70
|
+
data: bootstrapData,
|
|
71
|
+
});
|
|
72
|
+
const [threshold, owners] = (0, viem_1.decodeAbiParameters)([{ type: 'uint256' }, { type: 'address[]' }], bootstrap.args[0]);
|
|
73
|
+
(0, bun_test_1.expect)(threshold).toBe(1n);
|
|
74
|
+
(0, bun_test_1.expect)(owners).toEqual([profile.collectorAddress]);
|
|
75
|
+
// Source: deployed IntentExecutor addresses; the dev contract set must
|
|
76
|
+
// preserve the existing pot's executor rather than use SDK defaults.
|
|
77
|
+
(0, bun_test_1.expect)(bootstrap.args[2].map((executor) => executor.module.toLowerCase())).toEqual([
|
|
78
|
+
env === 'prod'
|
|
79
|
+
? '0x00000000005ad9ce1f5035fd62ca96cef16adaaf'
|
|
80
|
+
: '0xbf9b5b917a83f8adac17b0752846d41d8d7b7e17',
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
(0, bun_test_1.test)(`${env} published recipients match the custody profile`, () => {
|
|
84
|
+
const file = env === 'prod' ? 'contracts.json' : 'contracts.dev.json';
|
|
85
|
+
const contracts = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(root, 'shared-configs/configs', file), 'utf8'));
|
|
86
|
+
(0, bun_test_1.expect)(() => (0, app_fees_1.validateAppFeeContracts)(contracts, profiles[env])).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
(0, bun_test_1.test)(`${env} collector has a valid KMS ownership signature`, async () => {
|
|
89
|
+
await (0, app_fees_1.verifyOwnershipProof)(ownershipProof(env), env, profiles[env]);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
(0, bun_test_1.test)('refuses the empty-hash owner and a pot derived with a different owner', async () => {
|
|
93
|
+
await (0, bun_test_1.expect)((0, app_fees_1.deriveAppFeeAccount)({
|
|
94
|
+
...profiles.prod,
|
|
95
|
+
collectorAddress: emptyHashAddress,
|
|
96
|
+
})).rejects.toThrow('empty hash');
|
|
97
|
+
await (0, bun_test_1.expect)((0, app_fees_1.deriveAppFeeAccount)({
|
|
98
|
+
...profiles.prod,
|
|
99
|
+
potAddress: profiles.staging.potAddress,
|
|
100
|
+
})).rejects.toThrow('does not match SDK derivation');
|
|
101
|
+
});
|
|
102
|
+
(0, bun_test_1.test)('refuses a chain override that changes the collector', () => {
|
|
103
|
+
(0, bun_test_1.expect)(() => (0, app_fees_1.validateAppFeeContracts)({
|
|
104
|
+
'*': {
|
|
105
|
+
appFeeCollector: profiles.prod.collectorAddress,
|
|
106
|
+
appFeePot: profiles.prod.potAddress,
|
|
107
|
+
},
|
|
108
|
+
'1': { appFeeCollector: emptyHashAddress },
|
|
109
|
+
}, profiles.prod)).toThrow('chain 1');
|
|
110
|
+
});
|
|
111
|
+
(0, bun_test_1.test)('a modified challenge cannot reuse the recorded ownership signature', async () => {
|
|
112
|
+
const proof = ownershipProof('prod');
|
|
113
|
+
proof.message = proof.message.replace('Challenge: ', 'Challenge: altered-');
|
|
114
|
+
proof.digest = (0, viem_1.hashMessage)(proof.message);
|
|
115
|
+
await (0, bun_test_1.expect)((0, app_fees_1.verifyOwnershipProof)(proof, 'prod', profiles.prod)).rejects.toThrow('does not verify');
|
|
116
|
+
});
|
|
117
|
+
(0, bun_test_1.test)('KMS DER signatures recover correctly with either S normalization', async () => {
|
|
118
|
+
// Source: production KMS challenge signed 2026-10-20; Foundry independently
|
|
119
|
+
// verified this public fixture against the collector address.
|
|
120
|
+
const proof = ownershipProof('prod');
|
|
121
|
+
const r = BigInt(proof.signature.slice(0, 66));
|
|
122
|
+
const s = BigInt(`0x${proof.signature.slice(66, 130)}`);
|
|
123
|
+
for (const candidate of [s, secp256k1_1.secp256k1.CURVE.n - s]) {
|
|
124
|
+
const der = new secp256k1_1.secp256k1.Signature(r, candidate).toDERRawBytes();
|
|
125
|
+
(0, bun_test_1.expect)(await (0, app_fees_1.recoverOwnershipSignature)(proof.message, der, profiles.prod.collectorAddress)).toBe(proof.signature);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
(0, bun_test_1.test)('published swap contracts preserve their deployed addresses', () => {
|
|
129
|
+
// Source: published production/dev Plasma registry before this correction.
|
|
130
|
+
const expected = {
|
|
131
|
+
prod: [
|
|
132
|
+
'0x40CE38e0cbB8ec54a601256E4FacfED5679bccD0',
|
|
133
|
+
'0x5afce415b4370e5efd8b9be784d21c331beab965',
|
|
134
|
+
'0xaE448E141F230AC3c6F6dA981C0882356c9B91a6',
|
|
135
|
+
'0x3115c923734471b9da1d5bfef161d9c9e9963859',
|
|
136
|
+
],
|
|
137
|
+
staging: [
|
|
138
|
+
'0x8206052a213AA7cafB18ec7898e8D1D421C02100',
|
|
139
|
+
'0x21416a06e81fe115a5c8bf554b2b01383bd9b9f3',
|
|
140
|
+
'0x58BC7d9279A38a1537c2f74426dBDbD804AD3A18',
|
|
141
|
+
'0xc5f6641b3ade357c5df77a1859d16528fe8b3e93',
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
for (const env of ['prod', 'staging']) {
|
|
145
|
+
const file = env === 'prod' ? 'contracts.json' : 'contracts.dev.json';
|
|
146
|
+
const registry = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(root, 'shared-configs/configs', file), 'utf8'));
|
|
147
|
+
const contracts = { ...registry['*'], ...registry['9745'] };
|
|
148
|
+
(0, bun_test_1.expect)([
|
|
149
|
+
contracts.swapper,
|
|
150
|
+
contracts.tokenTransferProxy,
|
|
151
|
+
contracts.swapAdapter,
|
|
152
|
+
contracts.swapAdapterTokenSpender,
|
|
153
|
+
]).toEqual(expected[env]);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Address, type Hex } from 'viem';
|
|
2
|
+
export type AppFeeProfile = {
|
|
3
|
+
kmsKeyId: string;
|
|
4
|
+
collectorAddress: Address;
|
|
5
|
+
potAddress: Address;
|
|
6
|
+
nexusVersion: '1.2.0' | '1.2.1';
|
|
7
|
+
useDevContracts: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type AppFeeProfiles = Record<'prod' | 'staging', AppFeeProfile>;
|
|
10
|
+
export type OwnershipProof = {
|
|
11
|
+
environment: string;
|
|
12
|
+
keyId: string;
|
|
13
|
+
message: string;
|
|
14
|
+
digest: Hex;
|
|
15
|
+
signature: Hex;
|
|
16
|
+
};
|
|
17
|
+
export declare function loadAppFeeProfiles(yeetRoot: string): AppFeeProfiles;
|
|
18
|
+
export declare function collectorFromKmsPublicKey(response: {
|
|
19
|
+
KeySpec?: string;
|
|
20
|
+
PublicKey?: string;
|
|
21
|
+
}): Address;
|
|
22
|
+
export declare function ownershipChallenge(env: string, profile: AppFeeProfile): string;
|
|
23
|
+
export declare function recoverOwnershipSignature(message: string, der: Uint8Array, expected: Address): Promise<Hex>;
|
|
24
|
+
export declare function verifyOwnershipProof(proof: OwnershipProof, env: string, profile: AppFeeProfile): Promise<void>;
|
|
25
|
+
export declare function verifyAppFeeOwnershipProofs(yeetRoot: string, profiles: AppFeeProfiles): Promise<void>;
|
|
26
|
+
export declare function deriveAppFeeAccount(profile: AppFeeProfile): Promise<{
|
|
27
|
+
factory: Address;
|
|
28
|
+
factoryData: Hex;
|
|
29
|
+
collectorAddress: `0x${string}`;
|
|
30
|
+
potAddress: `0x${string}`;
|
|
31
|
+
nexusVersion: "1.2.0" | "1.2.1";
|
|
32
|
+
salt: `0x${string}`;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function deriveAppFeeAccounts(profiles: AppFeeProfiles): Promise<{
|
|
35
|
+
prod: {
|
|
36
|
+
factory: Address;
|
|
37
|
+
factoryData: Hex;
|
|
38
|
+
collectorAddress: `0x${string}`;
|
|
39
|
+
potAddress: `0x${string}`;
|
|
40
|
+
nexusVersion: "1.2.0" | "1.2.1";
|
|
41
|
+
salt: `0x${string}`;
|
|
42
|
+
};
|
|
43
|
+
staging: {
|
|
44
|
+
factory: Address;
|
|
45
|
+
factoryData: Hex;
|
|
46
|
+
collectorAddress: `0x${string}`;
|
|
47
|
+
potAddress: `0x${string}`;
|
|
48
|
+
nexusVersion: "1.2.0" | "1.2.1";
|
|
49
|
+
salt: `0x${string}`;
|
|
50
|
+
};
|
|
51
|
+
}>;
|
|
52
|
+
export declare function validateAppFeeContracts(contracts: Record<string, Record<string, string>>, profile: AppFeeProfile): void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadAppFeeProfiles = loadAppFeeProfiles;
|
|
7
|
+
exports.collectorFromKmsPublicKey = collectorFromKmsPublicKey;
|
|
8
|
+
exports.ownershipChallenge = ownershipChallenge;
|
|
9
|
+
exports.recoverOwnershipSignature = recoverOwnershipSignature;
|
|
10
|
+
exports.verifyOwnershipProof = verifyOwnershipProof;
|
|
11
|
+
exports.verifyAppFeeOwnershipProofs = verifyAppFeeOwnershipProofs;
|
|
12
|
+
exports.deriveAppFeeAccount = deriveAppFeeAccount;
|
|
13
|
+
exports.deriveAppFeeAccounts = deriveAppFeeAccounts;
|
|
14
|
+
exports.validateAppFeeContracts = validateAppFeeContracts;
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const secp256k1_1 = require("@noble/curves/secp256k1");
|
|
19
|
+
const sdk_1 = require("@rhinestone/sdk");
|
|
20
|
+
const viem_1 = require("viem");
|
|
21
|
+
const accounts_1 = require("viem/accounts");
|
|
22
|
+
function loadAppFeeProfiles(yeetRoot) {
|
|
23
|
+
return JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(yeetRoot, 'config/app-fees.json'), 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
function collectorAddress(value) {
|
|
26
|
+
const address = (0, viem_1.getAddress)(value);
|
|
27
|
+
const emptyHashAddress = (0, viem_1.getAddress)(`0x${(0, viem_1.keccak256)('0x').slice(-40)}`);
|
|
28
|
+
if (address === viem_1.zeroAddress || address === emptyHashAddress) {
|
|
29
|
+
throw new Error('App-fee collector must be a valid KMS identity, not zero or the empty hash');
|
|
30
|
+
}
|
|
31
|
+
return address;
|
|
32
|
+
}
|
|
33
|
+
function collectorFromKmsPublicKey(response) {
|
|
34
|
+
if (response.KeySpec !== 'ECC_SECG_P256K1' || !response.PublicKey) {
|
|
35
|
+
throw new Error('KMS must return an ECC_SECG_P256K1 public key');
|
|
36
|
+
}
|
|
37
|
+
const der = Buffer.from(response.PublicKey, 'base64');
|
|
38
|
+
// DER SPKI: id-ecPublicKey, secp256k1, uncompressed 65-byte point.
|
|
39
|
+
const header = Buffer.from('3056301006072a8648ce3d020106052b8104000a034200', 'hex');
|
|
40
|
+
if (der.length !== 88 || !der.subarray(0, header.length).equals(header)) {
|
|
41
|
+
throw new Error('KMS public key must be a secp256k1 SubjectPublicKeyInfo');
|
|
42
|
+
}
|
|
43
|
+
const point = secp256k1_1.secp256k1.ProjectivePoint.fromHex(der.subarray(header.length));
|
|
44
|
+
return collectorAddress((0, accounts_1.publicKeyToAddress)(`0x${Buffer.from(point.toRawBytes(false)).toString('hex')}`));
|
|
45
|
+
}
|
|
46
|
+
function ownershipChallenge(env, profile) {
|
|
47
|
+
return [
|
|
48
|
+
'Rhinestone app-fee collector ownership verification',
|
|
49
|
+
`Environment: ${env}`,
|
|
50
|
+
`KMS key: ${profile.kmsKeyId}`,
|
|
51
|
+
`Collector: ${profile.collectorAddress}`,
|
|
52
|
+
`Issued at: ${new Date().toISOString()}`,
|
|
53
|
+
`Challenge: ${(0, node_crypto_1.randomUUID)()}`,
|
|
54
|
+
'Purpose: prove signing control only. No transaction, intent, transfer, or spending authorization.',
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
async function recoverOwnershipSignature(message, der, expected) {
|
|
58
|
+
const { r, s } = secp256k1_1.secp256k1.Signature.fromDER(der).normalizeS();
|
|
59
|
+
for (const v of [27n, 28n]) {
|
|
60
|
+
const signature = (0, viem_1.serializeSignature)({
|
|
61
|
+
r: (0, viem_1.toHex)(r, { size: 32 }),
|
|
62
|
+
s: (0, viem_1.toHex)(s, { size: 32 }),
|
|
63
|
+
v,
|
|
64
|
+
});
|
|
65
|
+
if ((0, viem_1.getAddress)(await (0, viem_1.recoverMessageAddress)({ message, signature })) ===
|
|
66
|
+
(0, viem_1.getAddress)(expected)) {
|
|
67
|
+
return signature;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error('KMS ownership signature does not recover to the configured collector');
|
|
71
|
+
}
|
|
72
|
+
async function verifyOwnershipProof(proof, env, profile) {
|
|
73
|
+
const lines = proof.message.split('\n');
|
|
74
|
+
if (proof.environment !== env ||
|
|
75
|
+
proof.keyId !== profile.kmsKeyId ||
|
|
76
|
+
lines[0] !== 'Rhinestone app-fee collector ownership verification' ||
|
|
77
|
+
!lines.includes(`Environment: ${env}`) ||
|
|
78
|
+
!lines.includes(`KMS key: ${profile.kmsKeyId}`) ||
|
|
79
|
+
!lines.includes(`Collector: ${profile.collectorAddress}`) ||
|
|
80
|
+
(0, viem_1.hashMessage)(proof.message) !== proof.digest ||
|
|
81
|
+
(0, viem_1.getAddress)(await (0, viem_1.recoverMessageAddress)({
|
|
82
|
+
message: proof.message,
|
|
83
|
+
signature: proof.signature,
|
|
84
|
+
})) !== collectorAddress(profile.collectorAddress)) {
|
|
85
|
+
throw new Error(`App-fee ownership proof does not verify for ${env}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function verifyAppFeeOwnershipProofs(yeetRoot, profiles) {
|
|
89
|
+
for (const env of ['prod', 'staging']) {
|
|
90
|
+
const proof = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(yeetRoot, `docs/app-fee-collector.${env}.json`), 'utf8'));
|
|
91
|
+
await verifyOwnershipProof(proof, env, profiles[env]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function deriveAppFeeAccount(profile) {
|
|
95
|
+
if (!['1.2.0', '1.2.1'].includes(profile.nexusVersion) ||
|
|
96
|
+
typeof profile.useDevContracts !== 'boolean') {
|
|
97
|
+
throw new Error('App-fee account requires an explicit Nexus version and contract set');
|
|
98
|
+
}
|
|
99
|
+
const collector = collectorAddress(profile.collectorAddress);
|
|
100
|
+
const refuseSigning = async () => {
|
|
101
|
+
throw new Error('Address derivation cannot sign');
|
|
102
|
+
};
|
|
103
|
+
const owner = (0, accounts_1.toAccount)({
|
|
104
|
+
address: collector,
|
|
105
|
+
signMessage: refuseSigning,
|
|
106
|
+
signTransaction: refuseSigning,
|
|
107
|
+
signTypedData: refuseSigning,
|
|
108
|
+
});
|
|
109
|
+
const sdk = new sdk_1.RhinestoneSDK({
|
|
110
|
+
auth: { mode: 'apiKey', apiKey: 'offline-address-derivation' },
|
|
111
|
+
useDevContracts: profile.useDevContracts,
|
|
112
|
+
});
|
|
113
|
+
const account = await sdk.createAccount({
|
|
114
|
+
account: { type: 'nexus', version: profile.nexusVersion },
|
|
115
|
+
owners: { type: 'ecdsa', accounts: [owner] },
|
|
116
|
+
});
|
|
117
|
+
const pot = account.getAddress();
|
|
118
|
+
if ((0, viem_1.getAddress)(profile.potAddress) !== pot) {
|
|
119
|
+
throw new Error(`App-fee pot ${profile.potAddress} does not match SDK derivation ${pot}`);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
collectorAddress: collector,
|
|
123
|
+
potAddress: pot,
|
|
124
|
+
nexusVersion: profile.nexusVersion,
|
|
125
|
+
salt: (0, viem_1.keccak256)('0x'),
|
|
126
|
+
...account.getInitData(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function deriveAppFeeAccounts(profiles) {
|
|
130
|
+
return {
|
|
131
|
+
prod: await deriveAppFeeAccount(profiles.prod),
|
|
132
|
+
staging: await deriveAppFeeAccount(profiles.staging),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function validateAppFeeContracts(contracts, profile) {
|
|
136
|
+
const expectedCollector = collectorAddress(profile.collectorAddress);
|
|
137
|
+
const expectedPot = (0, viem_1.getAddress)(profile.potAddress);
|
|
138
|
+
for (const [chainId, entry] of Object.entries(contracts)) {
|
|
139
|
+
const merged = { ...contracts['*'], ...entry };
|
|
140
|
+
if ((0, viem_1.getAddress)(merged.appFeeCollector) !== expectedCollector ||
|
|
141
|
+
(0, viem_1.getAddress)(merged.appFeePot) !== expectedPot) {
|
|
142
|
+
throw new Error(`App-fee contract addresses on chain ${chainId} differ from the custody profile`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const viem_1 = require("viem");
|
|
10
|
+
const app_fees_1 = require("./app-fees");
|
|
11
|
+
async function main() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const envIndex = args.indexOf('--env');
|
|
14
|
+
const env = envIndex === -1 ? undefined : args[envIndex + 1];
|
|
15
|
+
if (env !== 'prod' && env !== 'staging') {
|
|
16
|
+
throw new Error('Usage: bun run check:app-fees --env <prod|staging> [--kms] [--sign]');
|
|
17
|
+
}
|
|
18
|
+
const yeetRoot = node_path_1.default.resolve(process.cwd(), '..');
|
|
19
|
+
const profiles = (0, app_fees_1.loadAppFeeProfiles)(yeetRoot);
|
|
20
|
+
await (0, app_fees_1.verifyAppFeeOwnershipProofs)(yeetRoot, profiles);
|
|
21
|
+
const accounts = await (0, app_fees_1.deriveAppFeeAccounts)(profiles);
|
|
22
|
+
const profile = profiles[env];
|
|
23
|
+
const file = env === 'prod' ? 'contracts.json' : 'contracts.dev.json';
|
|
24
|
+
(0, app_fees_1.validateAppFeeContracts)(JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join('configs', file), 'utf8')), profile);
|
|
25
|
+
let proof = {};
|
|
26
|
+
if (args.includes('--kms') || args.includes('--sign')) {
|
|
27
|
+
const response = JSON.parse((0, node_child_process_1.execFileSync)('aws', [
|
|
28
|
+
'kms',
|
|
29
|
+
'get-public-key',
|
|
30
|
+
'--key-id',
|
|
31
|
+
profile.kmsKeyId,
|
|
32
|
+
'--region',
|
|
33
|
+
'us-east-1',
|
|
34
|
+
'--output',
|
|
35
|
+
'json',
|
|
36
|
+
], { encoding: 'utf8' }));
|
|
37
|
+
if (response.KeyId !== profile.kmsKeyId) {
|
|
38
|
+
throw new Error('KMS returned a different key ID');
|
|
39
|
+
}
|
|
40
|
+
const collector = (0, app_fees_1.collectorFromKmsPublicKey)(response);
|
|
41
|
+
if (collector !== accounts[env].collectorAddress) {
|
|
42
|
+
throw new Error(`KMS collector ${collector} differs from configured collector ${profile.collectorAddress}`);
|
|
43
|
+
}
|
|
44
|
+
if (args.includes('--sign')) {
|
|
45
|
+
const message = (0, app_fees_1.ownershipChallenge)(env, profile);
|
|
46
|
+
const digest = (0, viem_1.hashMessage)(message);
|
|
47
|
+
const signed = JSON.parse((0, node_child_process_1.execFileSync)('aws', [
|
|
48
|
+
'kms',
|
|
49
|
+
'sign',
|
|
50
|
+
'--key-id',
|
|
51
|
+
profile.kmsKeyId,
|
|
52
|
+
'--region',
|
|
53
|
+
'us-east-1',
|
|
54
|
+
'--message-type',
|
|
55
|
+
'DIGEST',
|
|
56
|
+
'--signing-algorithm',
|
|
57
|
+
'ECDSA_SHA_256',
|
|
58
|
+
'--message',
|
|
59
|
+
Buffer.from(digest.slice(2), 'hex').toString('base64'),
|
|
60
|
+
'--cli-binary-format',
|
|
61
|
+
'base64',
|
|
62
|
+
'--output',
|
|
63
|
+
'json',
|
|
64
|
+
], { encoding: 'utf8' }));
|
|
65
|
+
if (signed.KeyId !== profile.kmsKeyId || !signed.Signature) {
|
|
66
|
+
throw new Error('KMS did not return a signature from the configured key');
|
|
67
|
+
}
|
|
68
|
+
const signature = await (0, app_fees_1.recoverOwnershipSignature)(message, Buffer.from(signed.Signature, 'base64'), collector);
|
|
69
|
+
proof = {
|
|
70
|
+
keyId: profile.kmsKeyId,
|
|
71
|
+
message,
|
|
72
|
+
digest,
|
|
73
|
+
signature,
|
|
74
|
+
recoveredCollector: collector,
|
|
75
|
+
verified: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const { collectorAddress, potAddress, nexusVersion, factory } = accounts[env];
|
|
80
|
+
console.log(JSON.stringify({
|
|
81
|
+
environment: env,
|
|
82
|
+
collectorAddress,
|
|
83
|
+
potAddress,
|
|
84
|
+
nexusVersion,
|
|
85
|
+
factory,
|
|
86
|
+
...proof,
|
|
87
|
+
}, null, 2));
|
|
88
|
+
}
|
|
89
|
+
main().catch((error) => {
|
|
90
|
+
console.error(error instanceof Error ? error.message : error);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
});
|
package/dist/scripts/generate.js
CHANGED
|
@@ -10,6 +10,7 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const validation_1 = require("./validation");
|
|
12
12
|
const hoistUniform_1 = require("./hoistUniform");
|
|
13
|
+
const app_fees_1 = require("./app-fees");
|
|
13
14
|
function renderNetworkEnum(name, entries) {
|
|
14
15
|
const lines = entries.map((entry) => entry.viemChain
|
|
15
16
|
? ` ${entry.enumKey} = ${entry.viemChain}.id,`
|
|
@@ -434,6 +435,9 @@ async function generate() {
|
|
|
434
435
|
const yeetRoot = node_path_1.default.resolve(sharedConfigsRoot, "..");
|
|
435
436
|
const configsDir = node_path_1.default.join(sharedConfigsRoot, "configs");
|
|
436
437
|
const generatedDir = node_path_1.default.join(sharedConfigsRoot, "src", "generated");
|
|
438
|
+
const appFeeProfiles = (0, app_fees_1.loadAppFeeProfiles)(yeetRoot);
|
|
439
|
+
await (0, app_fees_1.verifyAppFeeOwnershipProofs)(yeetRoot, appFeeProfiles);
|
|
440
|
+
const appFeeAccounts = await (0, app_fees_1.deriveAppFeeAccounts)(appFeeProfiles);
|
|
437
441
|
const output = runJsonnet(yeetRoot);
|
|
438
442
|
await (0, promises_1.mkdir)(configsDir, { recursive: true });
|
|
439
443
|
await (0, promises_1.mkdir)(generatedDir, { recursive: true });
|
|
@@ -538,6 +542,9 @@ async function generate() {
|
|
|
538
542
|
// (or breaks its uniformity) is reflected in what lands in the wildcard.
|
|
539
543
|
const prodContractsMerged = (0, hoistUniform_1.hoistUniformAddresses)(mergeContracts(prodContracts, overrides.prod ?? {}));
|
|
540
544
|
const stagingContractsMerged = (0, hoistUniform_1.hoistUniformAddresses)(mergeContracts(stagingContracts, devOverrides));
|
|
545
|
+
(0, app_fees_1.validateAppFeeContracts)(prodContractsMerged, appFeeProfiles.prod);
|
|
546
|
+
(0, app_fees_1.validateAppFeeContracts)(stagingContractsMerged, appFeeProfiles.staging);
|
|
547
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "app-fee-accounts.json"), `${JSON.stringify(appFeeAccounts, null, 2)}\n`);
|
|
541
548
|
await (0, promises_1.writeFile)(node_path_1.default.join(generatedDir, "contracts.ts"), `${renderContracts(prodContractsMerged)}\n`);
|
|
542
549
|
await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "contracts.json"), `${JSON.stringify(prodContractsMerged, null, 2)}\n`);
|
|
543
550
|
await (0, promises_1.writeFile)(node_path_1.default.join(generatedDir, "contracts.dev.ts"), `${renderContracts(stagingContractsMerged)}\n`);
|
|
@@ -245,8 +245,8 @@ const contractAddresses = {
|
|
|
245
245
|
"compact": "0x00000000000000171ede64904551eeDF3C6C9788",
|
|
246
246
|
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
247
247
|
"feeCollector": "0xfeebabe17996b3346cf80f04a1f072102a90cf09",
|
|
248
|
-
"appFeeCollector": "
|
|
249
|
-
"appFeePot": "
|
|
248
|
+
"appFeeCollector": "0x86DB034A82072141D286e4e8dfcE653B75b3B5bd",
|
|
249
|
+
"appFeePot": "0x2Aaac3FEd8b9A1e7114620D395ACF604772eF2B0",
|
|
250
250
|
"tokenTransferProxy": "0x5afce415b4370e5efd8b9be784d21c331beab965"
|
|
251
251
|
}
|
|
252
252
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const PACKAGE_VERSION = "2.36.
|
|
1
|
+
declare const PACKAGE_VERSION = "2.36.4";
|
|
2
2
|
export { PACKAGE_VERSION };
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PACKAGE_VERSION = void 0;
|
|
4
4
|
// Auto-generated by scripts/generate.ts. Do not edit manually.
|
|
5
|
-
const PACKAGE_VERSION = "2.36.
|
|
5
|
+
const PACKAGE_VERSION = "2.36.4";
|
|
6
6
|
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinestone/shared-configs",
|
|
3
|
-
"version": "2.36.
|
|
3
|
+
"version": "2.36.4",
|
|
4
4
|
"description": "Reusable configuration files for Rhinestone services",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rhinestone",
|
|
@@ -29,14 +29,17 @@
|
|
|
29
29
|
"sync:abis": "bun run scripts/sync-abis.ts",
|
|
30
30
|
"audit:token-authorizations": "bun run scripts/audit-token-authorizations.ts",
|
|
31
31
|
"check:vendor-drift": "bun run scripts/vendor-drift/main.ts",
|
|
32
|
+
"check:app-fees": "bun run scripts/check-app-fees.ts",
|
|
32
33
|
"compile": "tsc",
|
|
33
|
-
"test": "bun test",
|
|
34
|
+
"test": "bun test ./src ./scripts",
|
|
34
35
|
"build": "bun run clean && bun run generate && bun run sync:abis --generate && bun run compile",
|
|
35
36
|
"changeset:release": "bun run build && changeset publish"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@biomejs/biome": "^2.1.4",
|
|
39
40
|
"@changesets/cli": "^2.27.7",
|
|
41
|
+
"@noble/curves": "1.9.1",
|
|
42
|
+
"@rhinestone/sdk": "2.0.0",
|
|
40
43
|
"@types/bun": "latest",
|
|
41
44
|
"rimraf": "^6.0.1",
|
|
42
45
|
"typescript": "^5.6.3",
|