Skip to content

Commit 10e1139

Browse files
authored
feat(cow-shed): add ttl cache for RPC calls (#644)
1 parent 5dd3bf5 commit 10e1139

File tree

2 files changed

+360
-2
lines changed

2 files changed

+360
-2
lines changed

packages/cow-shed/src/contracts/CoWShedHooks.test.ts

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,4 +627,329 @@ describe('CowShedHooks', () => {
627627
}
628628
})
629629
})
630+
631+
describe('eip1271SignatureCache', () => {
632+
it('should cache EIP1271 signature verification results', async () => {
633+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
634+
635+
for (const adapterName of adapterNames) {
636+
const adapter = adapters[adapterName]
637+
setGlobalAdapter(adapter)
638+
639+
// Mock readContract to return EIP1271_MAGICVALUE
640+
const originalReadContract = adapter.readContract
641+
const readContractMock = jest.fn().mockResolvedValue('0x1626ba7e')
642+
adapter.readContract = readContractMock
643+
644+
const cowShed = new CowShedHooks(1, {
645+
factoryAddress: MOCK_COW_SHED_FACTORY,
646+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
647+
proxyCreationCode: MOCK_INIT_CODE,
648+
})
649+
650+
const calls = createCallsForAdapter(adapter)
651+
const nonce = adapter.utils.formatBytes32String('1')
652+
const deadline = BigInt(1000000)
653+
const proxy = cowShed.proxyOf(USER_MOCK)
654+
const typedDataContext = cowShed.infoToSign(calls, nonce, deadline, proxy)
655+
const signature = '0x1234abcd'
656+
657+
// First call - should hit the contract
658+
const result1 = await cowShed.verifyEip1271Signature(USER_MOCK, signature, typedDataContext)
659+
expect(result1).toBe(true)
660+
expect(readContractMock).toHaveBeenCalledTimes(1)
661+
662+
// Second call with same parameters - should use cache
663+
const result2 = await cowShed.verifyEip1271Signature(USER_MOCK, signature, typedDataContext)
664+
expect(result2).toBe(true)
665+
expect(readContractMock).toHaveBeenCalledTimes(1) // Still 1, not called again
666+
667+
// Third call with same parameters - should still use cache
668+
const result3 = await cowShed.verifyEip1271Signature(USER_MOCK, signature, typedDataContext)
669+
expect(result3).toBe(true)
670+
expect(readContractMock).toHaveBeenCalledTimes(1) // Still 1
671+
672+
// Restore original method
673+
adapter.readContract = originalReadContract
674+
}
675+
})
676+
677+
it('should cache negative EIP1271 signature verification results', async () => {
678+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
679+
680+
for (const adapterName of adapterNames) {
681+
const adapter = adapters[adapterName]
682+
setGlobalAdapter(adapter)
683+
684+
// Mock readContract to return invalid magic value
685+
const originalReadContract = adapter.readContract
686+
const readContractMock = jest.fn().mockResolvedValue('0xffffffff')
687+
adapter.readContract = readContractMock
688+
689+
const cowShed = new CowShedHooks(1, {
690+
factoryAddress: MOCK_COW_SHED_FACTORY,
691+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
692+
proxyCreationCode: MOCK_INIT_CODE,
693+
})
694+
695+
const calls = createCallsForAdapter(adapter)
696+
const nonce = adapter.utils.formatBytes32String('1')
697+
const deadline = BigInt(1000000)
698+
const proxy = cowShed.proxyOf(USER_MOCK)
699+
const typedDataContext = cowShed.infoToSign(calls, nonce, deadline, proxy)
700+
const signature = '0x1234abcd'
701+
702+
// First call - should hit the contract
703+
const result1 = await cowShed.verifyEip1271Signature(USER_MOCK, signature, typedDataContext)
704+
expect(result1).toBe(false)
705+
expect(readContractMock).toHaveBeenCalledTimes(1)
706+
707+
// Second call - should use cache
708+
const result2 = await cowShed.verifyEip1271Signature(USER_MOCK, signature, typedDataContext)
709+
expect(result2).toBe(false)
710+
expect(readContractMock).toHaveBeenCalledTimes(1) // Still 1
711+
712+
// Restore original method
713+
adapter.readContract = originalReadContract
714+
}
715+
})
716+
717+
it('should not use cache for different signatures', async () => {
718+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
719+
720+
for (const adapterName of adapterNames) {
721+
const adapter = adapters[adapterName]
722+
setGlobalAdapter(adapter)
723+
724+
// Mock readContract to return EIP1271_MAGICVALUE
725+
const originalReadContract = adapter.readContract
726+
const readContractMock = jest.fn().mockResolvedValue('0x1626ba7e')
727+
adapter.readContract = readContractMock
728+
729+
const cowShed = new CowShedHooks(1, {
730+
factoryAddress: MOCK_COW_SHED_FACTORY,
731+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
732+
proxyCreationCode: MOCK_INIT_CODE,
733+
})
734+
735+
const calls = createCallsForAdapter(adapter)
736+
const nonce = adapter.utils.formatBytes32String('1')
737+
const deadline = BigInt(1000000)
738+
const proxy = cowShed.proxyOf(USER_MOCK)
739+
const typedDataContext = cowShed.infoToSign(calls, nonce, deadline, proxy)
740+
741+
// First call with signature1
742+
const signature1 = '0x1234abcd'
743+
const result1 = await cowShed.verifyEip1271Signature(USER_MOCK, signature1, typedDataContext)
744+
expect(result1).toBe(true)
745+
expect(readContractMock).toHaveBeenCalledTimes(1)
746+
747+
// Second call with different signature - should hit contract again
748+
const signature2 = '0xdeadbeef'
749+
const result2 = await cowShed.verifyEip1271Signature(USER_MOCK, signature2, typedDataContext)
750+
expect(result2).toBe(true)
751+
expect(readContractMock).toHaveBeenCalledTimes(2) // Called again
752+
753+
// Restore original method
754+
adapter.readContract = originalReadContract
755+
}
756+
})
757+
758+
it('should not use cache for different accounts', async () => {
759+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
760+
761+
for (const adapterName of adapterNames) {
762+
const adapter = adapters[adapterName]
763+
setGlobalAdapter(adapter)
764+
765+
// Mock readContract to return EIP1271_MAGICVALUE
766+
const originalReadContract = adapter.readContract
767+
const readContractMock = jest.fn().mockResolvedValue('0x1626ba7e')
768+
adapter.readContract = readContractMock
769+
770+
const cowShed = new CowShedHooks(1, {
771+
factoryAddress: MOCK_COW_SHED_FACTORY,
772+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
773+
proxyCreationCode: MOCK_INIT_CODE,
774+
})
775+
776+
const calls = createCallsForAdapter(adapter)
777+
const nonce = adapter.utils.formatBytes32String('1')
778+
const deadline = BigInt(1000000)
779+
const proxy = cowShed.proxyOf(USER_MOCK)
780+
const typedDataContext = cowShed.infoToSign(calls, nonce, deadline, proxy)
781+
const signature = '0x1234abcd'
782+
783+
// First call with account1
784+
const account1 = USER_MOCK
785+
const result1 = await cowShed.verifyEip1271Signature(account1, signature, typedDataContext)
786+
expect(result1).toBe(true)
787+
expect(readContractMock).toHaveBeenCalledTimes(1)
788+
789+
// Second call with different account - should hit contract again
790+
const account2 = '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC'
791+
const result2 = await cowShed.verifyEip1271Signature(account2, signature, typedDataContext)
792+
expect(result2).toBe(true)
793+
expect(readContractMock).toHaveBeenCalledTimes(2) // Called again
794+
795+
// Restore original method
796+
adapter.readContract = originalReadContract
797+
}
798+
})
799+
})
800+
801+
describe('accountCodeCache', () => {
802+
it('should cache account code check results for smart contracts', async () => {
803+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
804+
805+
for (const adapterName of adapterNames) {
806+
const adapter = adapters[adapterName]
807+
setGlobalAdapter(adapter)
808+
809+
// Mock getCode to return bytecode
810+
const originalGetCode = adapter.getCode
811+
const getCodeMock = jest.fn().mockResolvedValue('0x608060405234801561001057600080fd5b50')
812+
adapter.getCode = getCodeMock
813+
814+
const cowShed = new CowShedHooks(1, {
815+
factoryAddress: MOCK_COW_SHED_FACTORY,
816+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
817+
proxyCreationCode: MOCK_INIT_CODE,
818+
})
819+
820+
// First call - should hit getCode
821+
const result1 = await (cowShed as any).doesAccountHaveCode(USER_MOCK)
822+
expect(result1).toBe(true)
823+
expect(getCodeMock).toHaveBeenCalledTimes(1)
824+
expect(getCodeMock).toHaveBeenCalledWith(USER_MOCK)
825+
826+
// Second call with same account - should use cache
827+
const result2 = await (cowShed as any).doesAccountHaveCode(USER_MOCK)
828+
expect(result2).toBe(true)
829+
expect(getCodeMock).toHaveBeenCalledTimes(1) // Still 1, not called again
830+
831+
// Third call - should still use cache
832+
const result3 = await (cowShed as any).doesAccountHaveCode(USER_MOCK)
833+
expect(result3).toBe(true)
834+
expect(getCodeMock).toHaveBeenCalledTimes(1) // Still 1
835+
836+
// Restore original method
837+
adapter.getCode = originalGetCode
838+
}
839+
})
840+
841+
it('should cache account code check results for EOA accounts', async () => {
842+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
843+
844+
for (const adapterName of adapterNames) {
845+
const adapter = adapters[adapterName]
846+
setGlobalAdapter(adapter)
847+
848+
// Mock getCode to return '0x' for EOA
849+
const originalGetCode = adapter.getCode
850+
const getCodeMock = jest.fn().mockResolvedValue('0x')
851+
adapter.getCode = getCodeMock
852+
853+
const cowShed = new CowShedHooks(1, {
854+
factoryAddress: MOCK_COW_SHED_FACTORY,
855+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
856+
proxyCreationCode: MOCK_INIT_CODE,
857+
})
858+
859+
// First call - should hit getCode
860+
const result1 = await (cowShed as any).doesAccountHaveCode(USER_MOCK)
861+
expect(result1).toBe(false)
862+
expect(getCodeMock).toHaveBeenCalledTimes(1)
863+
864+
// Second call - should use cache
865+
const result2 = await (cowShed as any).doesAccountHaveCode(USER_MOCK)
866+
expect(result2).toBe(false)
867+
expect(getCodeMock).toHaveBeenCalledTimes(1) // Still 1
868+
869+
// Restore original method
870+
adapter.getCode = originalGetCode
871+
}
872+
})
873+
874+
it('should not use cache for different accounts', async () => {
875+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
876+
877+
for (const adapterName of adapterNames) {
878+
const adapter = adapters[adapterName]
879+
setGlobalAdapter(adapter)
880+
881+
// Mock getCode to return bytecode
882+
const originalGetCode = adapter.getCode
883+
const getCodeMock = jest.fn().mockResolvedValue('0x608060405234801561001057600080fd5b50')
884+
adapter.getCode = getCodeMock
885+
886+
const cowShed = new CowShedHooks(1, {
887+
factoryAddress: MOCK_COW_SHED_FACTORY,
888+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
889+
proxyCreationCode: MOCK_INIT_CODE,
890+
})
891+
892+
// First call with account1
893+
const account1 = USER_MOCK
894+
const result1 = await (cowShed as any).doesAccountHaveCode(account1)
895+
expect(result1).toBe(true)
896+
expect(getCodeMock).toHaveBeenCalledTimes(1)
897+
898+
// Second call with different account - should hit getCode again
899+
const account2 = '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC'
900+
const result2 = await (cowShed as any).doesAccountHaveCode(account2)
901+
expect(result2).toBe(true)
902+
expect(getCodeMock).toHaveBeenCalledTimes(2) // Called again
903+
904+
// Third call with account1 again - should use cache
905+
const result3 = await (cowShed as any).doesAccountHaveCode(account1)
906+
expect(result3).toBe(true)
907+
expect(getCodeMock).toHaveBeenCalledTimes(2) // Still 2, used cache for account1
908+
909+
// Restore original method
910+
adapter.getCode = originalGetCode
911+
}
912+
})
913+
914+
it('should cache results used in signCalls method', async () => {
915+
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
916+
917+
for (const adapterName of adapterNames) {
918+
const adapter = adapters[adapterName]
919+
setGlobalAdapter(adapter)
920+
921+
// Mock getCode to return '0x' for EOA
922+
const originalGetCode = adapter.getCode
923+
const getCodeMock = jest.fn().mockResolvedValue('0x')
924+
adapter.getCode = getCodeMock
925+
926+
const cowShed = new CowShedHooks(1, {
927+
factoryAddress: MOCK_COW_SHED_FACTORY,
928+
implementationAddress: MOCK_COW_SHED_IMPLEMENTATION,
929+
proxyCreationCode: MOCK_INIT_CODE,
930+
})
931+
932+
const mockNonce = adapter.utils.formatBytes32String('1')
933+
const mockDeadline = BigInt(1000000)
934+
const calls = createCallsForAdapter(adapter)
935+
936+
// First signCalls - should call getCode once
937+
await cowShed.signCalls(calls, mockNonce, mockDeadline, SigningScheme.EIP712, adapter.signer)
938+
expect(getCodeMock).toHaveBeenCalledTimes(1)
939+
940+
// Second signCalls with different nonce - should use cached doesAccountHaveCode
941+
const mockNonce2 = adapter.utils.formatBytes32String('2')
942+
await cowShed.signCalls(calls, mockNonce2, mockDeadline, SigningScheme.EIP712, adapter.signer)
943+
expect(getCodeMock).toHaveBeenCalledTimes(1) // Still 1, cache was used
944+
945+
// Third signCalls with different deadline - should still use cache
946+
const mockDeadline2 = BigInt(2000000)
947+
await cowShed.signCalls(calls, mockNonce, mockDeadline2, SigningScheme.EIP712, adapter.signer)
948+
expect(getCodeMock).toHaveBeenCalledTimes(1) // Still 1
949+
950+
// Restore original method
951+
adapter.getCode = originalGetCode
952+
}
953+
})
954+
})
630955
})

0 commit comments

Comments
 (0)