Skip to content

Commit 1a0e1e0

Browse files
author
ci-bot
committed
fix decoding var && more unit tests
1 parent 4b15ed8 commit 1a0e1e0

File tree

6 files changed

+452
-7
lines changed

6 files changed

+452
-7
lines changed

libs/remix-debug/src/solidity-decoder/helpers/callTreeHelper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function callDepthChange (step, trace) {
3232
export function isConstructorExit (tree, step, scopeId, initialEntrystackIndex, stepDetail, isConstructor) {
3333
if (!isConstructor) return false // we are not in a constructor anyway
3434
const scope = tree.scopes[scopeId]
35+
if (scope.functionDefinition && scope.functionDefinition.kind === 'constructor') return false // we are not in a constructor anyway
3536
if (scope.firstStep === step) {
3637
// we are just entering the constructor
3738
return false
@@ -40,7 +41,7 @@ export function isConstructorExit (tree, step, scopeId, initialEntrystackIndex,
4041
return false
4142
}
4243
// Check if stack has returned to entry depth (or below, in case of cleanup)
43-
if (initialEntrystackIndex !== undefined && stepDetail.stack.length <= initialEntrystackIndex) {
44+
if (initialEntrystackIndex && stepDetail.stack.length <= initialEntrystackIndex) {
4445
console.log('Exiting constructor scope ', scopeId, ' at step ', step)
4546
return true
4647
}

libs/remix-debug/src/solidity-decoder/internalCallTree.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ async function buildTree (tree: InternalCallTree, step, scopeId, isCreation, fun
775775

776776
// Update symbolic stack based on opcode execution
777777
const previousSymbolicStack = tree.symbolicStackManager.getStackAtStep(step)
778-
if (stepDetail.stack.length !== previousSymbolicStack.length) {
778+
if (tree.debug && stepDetail.stack.length !== previousSymbolicStack.length) {
779779
console.warn('STACK SIZE MISMATCH at step ', step, ' opcode ', stepDetail.op, ' symbolic stack size ', previousSymbolicStack.length, ' actual stack size ', stepDetail.stack.length )
780780
}
781781

@@ -793,7 +793,7 @@ async function buildTree (tree: InternalCallTree, step, scopeId, isCreation, fun
793793
const internalfunctionCall = /*functionDefinition &&*/ (sourceLocation && sourceLocation.jump === 'i') /*&& functionDefinition.kind !== 'constructor'*/
794794
const isJumpOutOfFunction = /*functionDefinition &&*/ (sourceLocation && sourceLocation.jump === 'o') /*&& functionDefinition.kind !== 'constructor'*/
795795

796-
if (stepDetail.op === 'JUMP' && functionDefinition && internalfunctionCall && !tree.fnJumpDest[currentAddress + ' ' + functionDefinition.id]) {
796+
if (stepDetail.op === 'JUMP' && functionDefinition && functionDefinition.kind !== 'constructor' && internalfunctionCall && !tree.fnJumpDest[currentAddress + ' ' + functionDefinition.id]) {
797797
// record entry point for that function
798798
tree.fnJumpDest[currentAddress + ' ' + functionDefinition.id] = nextStepDetail && nextStepDetail.pc // JUMPDEST
799799
}
@@ -810,7 +810,7 @@ async function buildTree (tree: InternalCallTree, step, scopeId, isCreation, fun
810810
try {
811811
previousSourceLocation = null
812812
const newScopeId = scopeId === '' ? subScope.toString() : scopeId + '.' + subScope
813-
console.log('Entering new scope at step ', step, newScopeId, isInternalTxInstrn, internalfunctionCall)
813+
if (tree.debug) console.log('Entering new scope at step ', step, newScopeId, isInternalTxInstrn, internalfunctionCall)
814814
tree.scopeStarts[step] = newScopeId
815815
const startExecutionLine = lineColumnPos && lineColumnPos.start ? lineColumnPos.start.line + 1 : undefined
816816
tree.scopes[newScopeId] = { firstStep: step, locals: {}, isCreation, gasCost: 0, startExecutionLine, functionDefinition: null, opcodeInfo: stepDetail, stackBeforeJumping: newSymbolicStack, lowLevelScope: true }
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
'use strict'
2+
import { NestedScope } from '@remix-project/remix-debug'
3+
import * as helper from './helper'
4+
const { ethers } = require('ethers')
5+
6+
module.exports = async function (st, privateKey, contractBytecode, compilationResult, contractCode) {
7+
st.plan(26)
8+
const enableCtorTest = false
9+
// Test scenarios with expected parameter values
10+
const testCases = [
11+
{
12+
name: 'directCall',
13+
signature: 'directCall(uint256,string)',
14+
params: [123, 'DirectTest'],
15+
description: 'Direct function call parameters'
16+
},
17+
{
18+
name: 'internalCallTest',
19+
signature: 'internalCallTest(uint256,string)',
20+
params: [456, 'InternalTest'],
21+
description: 'Internal call parameters'
22+
},
23+
{
24+
name: 'thisCallTest',
25+
signature: 'thisCallTest(uint256,string)',
26+
params: [789, 'ThisTest'],
27+
description: 'This.function() call parameters'
28+
},
29+
{
30+
name: 'callTest',
31+
signature: 'callTest(uint256,string)',
32+
params: [111, 'CallTest'],
33+
description: 'CALL operation parameters'
34+
},
35+
{
36+
name: 'staticCallTest',
37+
signature: 'staticCallTest(uint256,string)',
38+
params: [222, 'StaticTest'],
39+
description: 'STATICCALL operation parameters'
40+
},
41+
{
42+
name: 'delegateCallTest',
43+
signature: 'delegateCallTest(uint256,string)',
44+
params: [333, 'DelegateTest'],
45+
description: 'DELEGATECALL operation parameters'
46+
},
47+
{
48+
name: 'createTest',
49+
signature: 'createTest(uint256,string)',
50+
params: [444, 'CreateTest'],
51+
description: 'CREATE operation parameters'
52+
},
53+
{
54+
name: 'create2Test',
55+
signature: 'create2Test(uint256,string,bytes32)',
56+
params: [555, 'Create2Test', '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'],
57+
description: 'CREATE2 operation parameters'
58+
}
59+
]
60+
61+
// Helper function to encode parameters
62+
function encodeParams(signature: string, params: any[]): string {
63+
// Use ethers interface to encode function call
64+
const iface = new ethers.Interface([`function ${signature}`])
65+
const functionName = signature.split('(')[0]
66+
return iface.encodeFunctionData(functionName, params)
67+
}
68+
69+
// Helper function to find scope by function name and get its firstStep
70+
function findFunctionScope(nestedScopes: NestedScope[], functionName: string): { scope: NestedScope, firstStep: number } | null {
71+
let foundDuplicate = false // TODO this needs to be fixed, there are always 2 nodes for a single function call
72+
function traverse(scope: NestedScope, parentFirstStep?: number): { scope: NestedScope, firstStep: number } | null {
73+
// Check if this scope matches our function
74+
if (scope.functionDefinition?.name === functionName || scope.functionDefinition?.kind === functionName) {
75+
const firstStep = scope.firstStep || parentFirstStep || 0
76+
return { scope, firstStep }
77+
}
78+
79+
// Look for nested function calls within this scope
80+
if (scope.children) {
81+
for (const child of scope.children) {
82+
const result = traverse(child, scope.firstStep || parentFirstStep)
83+
if (result && foundDuplicate) return result
84+
if (result) foundDuplicate = true
85+
}
86+
}
87+
88+
return null
89+
}
90+
91+
for (const rootScope of nestedScopes) {
92+
const result = traverse(rootScope)
93+
if (result) return result
94+
}
95+
96+
return null
97+
}
98+
99+
try {
100+
if (enableCtorTest) {
101+
// First deploy the contract (constructor with inheritance test)
102+
console.log('Deploying contract with constructor parameters...')
103+
const constructorParams = [42, 'ConstructorTest']
104+
const constructorData = ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'string'], constructorParams)
105+
const deployBytecode = contractBytecode + constructorData.slice(2) // Remove 0x prefix
106+
107+
const { traceManager: deployTraceManager, callTree: deployCallTree, waitForCallTree: waitForDeployCallTree } =
108+
await helper.setupDebugger(privateKey, deployBytecode, compilationResult, contractCode)
109+
110+
const { scopes: deployScopes, scopeStarts: deployScopeStarts } = await waitForDeployCallTree()
111+
const deployNestedScopes: NestedScope[] = deployCallTree.getScopesAsNestedJSON()
112+
113+
console.log(deployNestedScopes)
114+
115+
// Test inherited constructor parameters
116+
const baseConstructorScope = findFunctionScope(deployNestedScopes, 'constructor')
117+
if (baseConstructorScope) {
118+
console.log(`Testing constructor parameters at step ${baseConstructorScope.firstStep}`)
119+
120+
const symbolicStack = deployCallTree.getSymbolicStackAtStep(baseConstructorScope.firstStep)
121+
console.log('Constructor symbolic stack:', symbolicStack)
122+
123+
await helper.decodeLocals(st, baseConstructorScope.firstStep + 1, deployTraceManager, deployCallTree, (locals) => {
124+
console.log('Constructor locals:', Object.keys(locals))
125+
126+
// Look for constructor parameters
127+
if (locals['_constructorValue']) {
128+
st.equals(locals['_constructorValue'].value, '42', 'Constructor uint parameter should be decoded correctly')
129+
}
130+
if (locals['_constructorMessage']) {
131+
st.equals(locals['_constructorMessage'].value, 'ConstructorTest', 'Constructor string parameter should be decoded correctly')
132+
}
133+
134+
// Also check inherited constructor parameters
135+
if (locals['_baseValue']) {
136+
st.equals(locals['_baseValue'].value, '52', 'Base constructor uint parameter should be decoded correctly (42 + 10)')
137+
}
138+
if (locals['_baseMessage']) {
139+
st.equals(locals['_baseMessage'].value, 'Base: ConstructorTest', 'Base constructor string parameter should be decoded correctly')
140+
}
141+
142+
st.ok(Object.keys(locals).length > 0, 'Constructor should have decoded local variables')
143+
})
144+
} else {
145+
st.fail('Could not find constructor scope for parameter testing')
146+
}
147+
}
148+
149+
// Now test each function call scenario
150+
for (const testCase of testCases) {
151+
console.log(`\nTesting ${testCase.name}: ${testCase.description}`)
152+
153+
const txData = encodeParams(testCase.signature, testCase.params)
154+
console.log(`Transaction data for ${testCase.name}: ${txData}`)
155+
156+
const constructorParams = [42, 'ConstructorTest']
157+
const constructorData = ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'string'], constructorParams)
158+
const deployBytecode = contractBytecode + constructorData.slice(2) // Remove 0x prefix
159+
const { traceManager, callTree, waitForCallTree } = await helper.setupDebugger(
160+
privateKey,
161+
deployBytecode,
162+
compilationResult,
163+
contractCode,
164+
txData
165+
)
166+
167+
const { scopes, scopeStarts } = await waitForCallTree()
168+
const nestedScopes: NestedScope[] = callTree.getScopesAsNestedJSON()
169+
170+
// Find the target function scope
171+
const functionScope = findFunctionScope(nestedScopes, testCase.name)
172+
173+
if (functionScope) {
174+
console.log(`Found ${testCase.name} scope with firstStep: ${functionScope.firstStep}`)
175+
176+
// Get symbolic stack at the first step of this function
177+
const symbolicStack = callTree.getSymbolicStackAtStep(functionScope.firstStep)
178+
// console.log(`Symbolic stack for ${testCase.name}:`, symbolicStack)
179+
180+
// Decode locals at this step
181+
await helper.decodeLocals(st, functionScope.firstStep + 2, traceManager, callTree, (locals) => {
182+
console.log(`${testCase.name} locals:`, locals)
183+
184+
// Verify parameter decoding based on function signature
185+
if (testCase.signature.includes('uint256')) {
186+
const uintParamNames = ['_directValue', '_testValue', '_thisValue', '_callValue', '_staticValue', '_delegateValue', '_createValue', '_create2Value']
187+
const expectedUintParam = uintParamNames.find(name => locals[name])
188+
189+
if (expectedUintParam) {
190+
st.equals(locals[expectedUintParam].value, testCase.params[0].toString(),
191+
`${testCase.name}: uint parameter should be decoded correctly`)
192+
} else {
193+
// Try alternative parameter names
194+
const allUintKeys = Object.keys(locals).filter(key =>
195+
key.includes('Value') || key.includes('value') || locals[key].type?.includes('uint'))
196+
if (allUintKeys.length > 0) {
197+
st.ok(true, `${testCase.name}: Found potential uint parameter: ${allUintKeys[0]}`)
198+
} else {
199+
st.fail(`${testCase.name}: Could not find uint parameter in locals`)
200+
}
201+
}
202+
}
203+
204+
if (testCase.signature.includes('string')) {
205+
const stringParamNames = ['_directMessage', '_testMessage', '_thisMessage', '_callMessage', '_staticMessage', '_delegateMessage', '_createMessage', '_create2Message']
206+
const expectedStringParam = stringParamNames.find(name => locals[name])
207+
208+
if (expectedStringParam) {
209+
st.equals(locals[expectedStringParam].value, testCase.params[1],
210+
`${testCase.name}: string parameter should be decoded correctly`)
211+
} else {
212+
// Try alternative parameter names
213+
const allStringKeys = Object.keys(locals).filter(key =>
214+
key.includes('Message') || key.includes('message') || locals[key].type?.includes('string'))
215+
if (allStringKeys.length > 0) {
216+
st.ok(true, `${testCase.name}: Found potential string parameter: ${allStringKeys[0]}`)
217+
} else {
218+
st.fail(`${testCase.name}: Could not find string parameter in locals`)
219+
}
220+
}
221+
}
222+
223+
st.ok(Object.keys(locals).length > 0, `${testCase.name}: Should have decoded local variables`)
224+
})
225+
226+
// Additional tests for internal function calls
227+
if (testCase.name === 'internalCallTest') {
228+
// Also check the internal function that gets called
229+
const internalScope = findFunctionScope(nestedScopes, '_internalFunction')
230+
if (internalScope) {
231+
console.log(`Found _internalFunction scope with firstStep: ${internalScope.firstStep}`)
232+
await helper.decodeLocals(st, internalScope.firstStep + 2, traceManager, callTree, (locals) => {
233+
console.log('_internalFunction locals:', Object.keys(locals))
234+
235+
if (locals['_internalValue']) {
236+
st.equals(locals['_internalValue'].value, '506', 'Internal function uint parameter should be decoded correctly (456 + 50)')
237+
}
238+
if (locals['_internalMessage']) {
239+
st.ok(locals['_internalMessage'].value.includes('Internal:'), 'Internal function string parameter should be decoded correctly')
240+
}
241+
})
242+
}
243+
}
244+
245+
// For external calls via this.function(), check the target function too
246+
if (testCase.name === 'thisCallTest') {
247+
const externalScope = findFunctionScope(nestedScopes, 'externalCallViaThis')
248+
if (externalScope) {
249+
console.log(`Found externalCallViaThis scope with firstStep: ${externalScope.firstStep}`)
250+
await helper.decodeLocals(st, externalScope.firstStep + 2, traceManager, callTree, (locals) => {
251+
console.log('externalCallViaThis locals:', Object.keys(locals))
252+
253+
if (locals['_externalValue']) {
254+
st.equals(locals['_externalValue'].value, '814', 'External function uint parameter should be decoded correctly (789 + 25)')
255+
}
256+
if (locals['_externalMessage']) {
257+
st.ok(locals['_externalMessage'].value.includes('This:'), 'External function string parameter should be decoded correctly')
258+
}
259+
})
260+
}
261+
}
262+
263+
} else {
264+
st.fail(`Could not find scope for function ${testCase.name}`)
265+
}
266+
}
267+
268+
} catch (error) {
269+
console.error('Test error:', error)
270+
st.fail(error.message || error)
271+
}
272+
}

0 commit comments

Comments
 (0)