|
| 1 | +import { decode } from '@metamask/abi-utils'; |
1 | 2 | import { |
2 | 3 | IsAtomicBatchSupportedRequest, |
3 | 4 | TransactionController, |
4 | 5 | TransactionMeta, |
5 | 6 | } from '@metamask/transaction-controller'; |
6 | 7 | import { SignMessenger, getDelegationTransaction } from './delegation'; |
7 | 8 | import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger'; |
8 | | -import { Hex } from '@metamask/utils'; |
| 9 | +import { Hex, bytesToHex, remove0x } from '@metamask/utils'; |
| 10 | +import { |
| 11 | + BATCH_DEFAULT_MODE, |
| 12 | + SINGLE_DEFAULT_MODE, |
| 13 | + getDeleGatorEnvironment, |
| 14 | +} from '../../core/Delegation'; |
| 15 | +import { |
| 16 | + REDEEM_DELEGATIONS_SELECTOR, |
| 17 | + encodeRedeemDelegations, |
| 18 | +} from '../../core/Delegation/delegation'; |
9 | 19 |
|
10 | 20 | const mockGetNonceLock = jest.fn(); |
11 | 21 |
|
@@ -178,5 +188,181 @@ describe('Transaction Delegation Utils', () => { |
178 | 188 | getDelegationTransaction(messengerMock, TRANSACTION_META_MOCK), |
179 | 189 | ).rejects.toThrow('Upgrade contract address not found'); |
180 | 190 | }); |
| 191 | + |
| 192 | + describe('nested redeemDelegations flattening', () => { |
| 193 | + const MAINNET_DELEGATION_MANAGER = getDeleGatorEnvironment( |
| 194 | + 1, |
| 195 | + ).DelegationManager.toLowerCase() as Hex; |
| 196 | + |
| 197 | + const INNER_CONTEXT = '0xaabb' as Hex; |
| 198 | + const INNER_CALLDATA = '0xccdd' as Hex; |
| 199 | + |
| 200 | + const buildInnerRedeemCalldata = (): Hex => |
| 201 | + encodeRedeemDelegations({ |
| 202 | + delegations: [], |
| 203 | + modes: [], |
| 204 | + executions: [], |
| 205 | + extraContexts: [INNER_CONTEXT], |
| 206 | + extraModes: [SINGLE_DEFAULT_MODE], |
| 207 | + extraCalldatas: [INNER_CALLDATA], |
| 208 | + }); |
| 209 | + |
| 210 | + const decodeOuter = ( |
| 211 | + outerData: Hex, |
| 212 | + ): { contexts: Hex[]; modes: Hex[]; calldatas: Hex[] } => { |
| 213 | + const payload = `0x${remove0x(outerData).slice(8)}` as Hex; |
| 214 | + const [contexts, modes, calldatas] = decode( |
| 215 | + ['bytes[]', 'bytes32[]', 'bytes[]'], |
| 216 | + payload, |
| 217 | + ) as [Uint8Array[], Uint8Array[], Uint8Array[]]; |
| 218 | + return { |
| 219 | + contexts: contexts.map(bytesToHex), |
| 220 | + modes: modes.map(bytesToHex), |
| 221 | + calldatas: calldatas.map(bytesToHex), |
| 222 | + }; |
| 223 | + }; |
| 224 | + |
| 225 | + it('flattens an inner redeemDelegations into additional outer slots', async () => { |
| 226 | + const innerRedeem = buildInnerRedeemCalldata(); |
| 227 | + const regularTx = { |
| 228 | + data: '0xdeadbeef' as Hex, |
| 229 | + to: '0x1111111111111111111111111111111111111111' as Hex, |
| 230 | + }; |
| 231 | + |
| 232 | + const result = await getDelegationTransaction(messengerMock, { |
| 233 | + ...TRANSACTION_META_MOCK, |
| 234 | + nestedTransactions: [ |
| 235 | + { |
| 236 | + data: innerRedeem, |
| 237 | + to: MAINNET_DELEGATION_MANAGER, |
| 238 | + }, |
| 239 | + regularTx, |
| 240 | + ], |
| 241 | + } as TransactionMeta); |
| 242 | + |
| 243 | + const outer = decodeOuter(result.data); |
| 244 | + |
| 245 | + expect(outer.contexts).toHaveLength(2); |
| 246 | + expect(outer.modes).toHaveLength(2); |
| 247 | + expect(outer.calldatas).toHaveLength(2); |
| 248 | + expect(outer.contexts[1]).toBe(INNER_CONTEXT); |
| 249 | + expect(outer.modes[1]).toBe(SINGLE_DEFAULT_MODE); |
| 250 | + expect(outer.calldatas[1]).toBe(INNER_CALLDATA); |
| 251 | + }); |
| 252 | + |
| 253 | + it('signs the outer delegation only over the non-redeem nested transactions', async () => { |
| 254 | + const innerRedeem = buildInnerRedeemCalldata(); |
| 255 | + const regularTx = { |
| 256 | + data: '0xdeadbeef' as Hex, |
| 257 | + to: '0x1111111111111111111111111111111111111111' as Hex, |
| 258 | + }; |
| 259 | + |
| 260 | + await getDelegationTransaction(messengerMock, { |
| 261 | + ...TRANSACTION_META_MOCK, |
| 262 | + nestedTransactions: [ |
| 263 | + { data: innerRedeem, to: MAINNET_DELEGATION_MANAGER }, |
| 264 | + regularTx, |
| 265 | + ], |
| 266 | + } as TransactionMeta); |
| 267 | + |
| 268 | + expect(signDelegationMock).toHaveBeenCalledTimes(1); |
| 269 | + const [signCall] = signDelegationMock.mock.calls; |
| 270 | + const signedDelegation = signCall[0].delegation; |
| 271 | + expect(signedDelegation.caveats).toHaveLength(2); |
| 272 | + }); |
| 273 | + |
| 274 | + it('uses SINGLE mode when only one regular nested transaction remains after flattening', async () => { |
| 275 | + const innerRedeem = buildInnerRedeemCalldata(); |
| 276 | + const regularTx = { |
| 277 | + data: '0xdeadbeef' as Hex, |
| 278 | + to: '0x1111111111111111111111111111111111111111' as Hex, |
| 279 | + }; |
| 280 | + |
| 281 | + const result = await getDelegationTransaction(messengerMock, { |
| 282 | + ...TRANSACTION_META_MOCK, |
| 283 | + nestedTransactions: [ |
| 284 | + { data: innerRedeem, to: MAINNET_DELEGATION_MANAGER }, |
| 285 | + regularTx, |
| 286 | + ], |
| 287 | + } as TransactionMeta); |
| 288 | + |
| 289 | + const outer = decodeOuter(result.data); |
| 290 | + expect(outer.modes[0]).toBe(SINGLE_DEFAULT_MODE); |
| 291 | + }); |
| 292 | + |
| 293 | + it('does not flatten nested redeemDelegations when target is not the DelegationManager', async () => { |
| 294 | + const innerRedeem = buildInnerRedeemCalldata(); |
| 295 | + const result = await getDelegationTransaction(messengerMock, { |
| 296 | + ...TRANSACTION_META_MOCK, |
| 297 | + nestedTransactions: [ |
| 298 | + { |
| 299 | + data: innerRedeem, |
| 300 | + to: '0x2222222222222222222222222222222222222222' as Hex, |
| 301 | + }, |
| 302 | + ], |
| 303 | + } as TransactionMeta); |
| 304 | + |
| 305 | + const outer = decodeOuter(result.data); |
| 306 | + expect(outer.contexts).toHaveLength(1); |
| 307 | + expect(signDelegationMock).toHaveBeenCalledTimes(1); |
| 308 | + }); |
| 309 | + |
| 310 | + it('does not flatten when nested tx data does not start with the redeemDelegations selector', async () => { |
| 311 | + const result = await getDelegationTransaction(messengerMock, { |
| 312 | + ...TRANSACTION_META_MOCK, |
| 313 | + nestedTransactions: [ |
| 314 | + { |
| 315 | + data: '0xdeadbeef' as Hex, |
| 316 | + to: MAINNET_DELEGATION_MANAGER, |
| 317 | + }, |
| 318 | + ], |
| 319 | + } as TransactionMeta); |
| 320 | + |
| 321 | + const outer = decodeOuter(result.data); |
| 322 | + expect(outer.contexts).toHaveLength(1); |
| 323 | + }); |
| 324 | + |
| 325 | + it('passes through inner contexts unchanged when only inner redeemDelegations are present', async () => { |
| 326 | + const innerRedeem = buildInnerRedeemCalldata(); |
| 327 | + const result = await getDelegationTransaction(messengerMock, { |
| 328 | + ...TRANSACTION_META_MOCK, |
| 329 | + nestedTransactions: [ |
| 330 | + { data: innerRedeem, to: MAINNET_DELEGATION_MANAGER }, |
| 331 | + ], |
| 332 | + } as TransactionMeta); |
| 333 | + |
| 334 | + const outer = decodeOuter(result.data); |
| 335 | + expect(outer.contexts).toEqual([INNER_CONTEXT]); |
| 336 | + expect(outer.modes).toEqual([SINGLE_DEFAULT_MODE]); |
| 337 | + expect(outer.calldatas).toEqual([INNER_CALLDATA]); |
| 338 | + expect(signDelegationMock).not.toHaveBeenCalled(); |
| 339 | + }); |
| 340 | + |
| 341 | + it('selects the correct selector for a redeemDelegations nested tx', () => { |
| 342 | + expect(REDEEM_DELEGATIONS_SELECTOR).toBe('0xcef6d209'); |
| 343 | + }); |
| 344 | + |
| 345 | + it('uses BATCH mode when multiple regular nested transactions remain after flattening', async () => { |
| 346 | + const innerRedeem = buildInnerRedeemCalldata(); |
| 347 | + const result = await getDelegationTransaction(messengerMock, { |
| 348 | + ...TRANSACTION_META_MOCK, |
| 349 | + nestedTransactions: [ |
| 350 | + { data: innerRedeem, to: MAINNET_DELEGATION_MANAGER }, |
| 351 | + { |
| 352 | + data: '0xdeadbeef' as Hex, |
| 353 | + to: '0x1111111111111111111111111111111111111111' as Hex, |
| 354 | + }, |
| 355 | + { |
| 356 | + data: '0xfeedface' as Hex, |
| 357 | + to: '0x3333333333333333333333333333333333333333' as Hex, |
| 358 | + }, |
| 359 | + ], |
| 360 | + } as TransactionMeta); |
| 361 | + |
| 362 | + const outer = decodeOuter(result.data); |
| 363 | + expect(outer.modes[0]).toBe(BATCH_DEFAULT_MODE); |
| 364 | + expect(outer.contexts).toHaveLength(2); |
| 365 | + }); |
| 366 | + }); |
181 | 367 | }); |
182 | 368 | }); |
0 commit comments