Skip to content

Commit

Permalink
User defined mapping fix and test cases for structureToSignatureType (#…
Browse files Browse the repository at this point in the history
…50)

* Move getStatesFromSourceCode to utils.

* Test cases for getStatesFromSourceCode with declarations from dataTypeParser test.

* Added more state declarations in test.

* Fix for states of map with structs.

* Add test case for constant and immutable state variables.

* Exclude constant variables from states.

Co-authored-by: nikugogoi <95nikass@gmail.com>
  • Loading branch information
ashwinphatak and nikugogoi authored Apr 29, 2021
1 parent 9da2fb4 commit 25ed9c7
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 71 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 2 additions & 29 deletions src/services/contractService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@

import { StateVariableDeclaration, StructDefinition } from 'solidity-parser-diligence';
import { getConnection } from 'typeorm';
import Contract from '../models/contract/contract';
import Event from '../models/contract/event';
Expand All @@ -12,11 +10,10 @@ import MethodRepository from '../repositories/contract/methodRepository';
import StateRepository from '../repositories/contract/stateRepository';
import AddressRepository from '../repositories/data/addressRepository';
import { ABI } from "../types";
import { structureToSignatureType } from './dataTypeParser';
import ApplicationError from "../errors/applicationError";
import { getStatesFromSourceCode } from '../utils/contract';

const childProcess = require('child_process'); // eslint-disable-line
const parser = require('@solidity-parser/parser'); // eslint-disable-line

type ContractParam = {
address: string;
Expand Down Expand Up @@ -95,7 +92,7 @@ export default class ContractService {

// prepare states
const stateIds: number[] = [];
const stateObjects = await this.getStatesFromSourceCode(contractObj.sourceCode);
const stateObjects = await getStatesFromSourceCode(contractObj.sourceCode);
for (const stateObject of stateObjects) {
const state = await stateRepository.add({
slot: stateObject.slot,
Expand Down Expand Up @@ -157,30 +154,6 @@ export default class ContractService {
})
}

private async getStatesFromSourceCode(sourceCode: string): Promise<State[]> {
const ast = parser.parse(sourceCode, {
tolerant: true,
});

let list = [];
const contractDefinitions = ast?.children?.filter((item) => item.type === 'ContractDefinition');
for (const contractDefinition of contractDefinitions) {
const states = contractDefinition?.subNodes.filter(n => n.type == 'StateVariableDeclaration') as StateVariableDeclaration[];
const structs = contractDefinition?.subNodes.filter(n => n.type == 'StructDefinition') as StructDefinition[];

list = list.concat(states?.map((item, slot) => {
const type: string = structureToSignatureType(item.variables[0]?.name, item.variables[0]?.typeName, structs).signature;
return {
slot,
type,
variable: item.variables[0]?.name,
}
}));
}

return list as State[];
}

private getEventsFromABI(abi: ABI): string[] {
if (!abi) {
return [];
Expand Down
40 changes: 0 additions & 40 deletions src/services/dataTypeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,46 +97,6 @@ function parseStructure(name: string, typeName: TypeName, structs: StructDefinit
}
}

export function structureToSignatureType(name: string, typeName: TypeName, structs: StructDefinition[], level = 0, isArray = false): {signature: string; type: string; hasStruct: boolean} {
let structsDef = '';
if (level === 0 && structs && structs.length) {
for (const struct of structs) {
structsDef += ` struct ${struct.name} {` + struct.members.map(m => structureToSignatureType(m.name, m.typeName, structs, level + 1).signature).join('') + '}';
}
}

switch (typeName.type) {
case 'ElementaryTypeName':
return {
signature: `${typeName.name}${isArray ? '[]' : ''} ${name};`,
type: typeName.name,
hasStruct: false,
}
case 'ArrayTypeName': {
const res = structureToSignatureType(name, typeName.baseTypeName, structs, level + 1, true);
return {
signature: res.signature + (level === 0 && res.hasStruct ? structsDef : ''),
type: typeName.baseTypeName?.type,
hasStruct: res.hasStruct,
}
}
case 'Mapping': {
const res = structureToSignatureType(name, typeName.valueType, structs, level + 1);
return {
signature: `mapping (${typeName.keyType.name} => ${res.type}) ${name};` + (level === 0 && res.hasStruct ? structsDef : ''),
type: `mapping (${typeName.keyType.name} => ${res.type})`, // for mapping => mapping case
hasStruct: res.hasStruct,
}
}
case 'UserDefinedTypeName':
return {
signature: `${typeName.namePath}${isArray ? '[]' : ''} ${name};` + (level === 0 ? structsDef : ''),
type: typeName.type,
hasStruct: true,
};
}
}

/**
* Parse structure from list of variables and types
* ```typescript
Expand Down
187 changes: 187 additions & 0 deletions src/utils/contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {getStatesFromSourceCode} from "./contract";

describe('getStatesfromSourceCode', () => {
test('elementary types', async () => {
const sourceCode = `
contract Test {
string public name = "Uniswap";
uint8 public decimals = 18;
int256 public integers = -21;
bool internal boolean = true;
address private plainAddress = '0x1ca7c995f8eF0A2989BbcE08D5B7Efe50A584aa1'
address payable payableAddress = '0xbb38B6F181541a6cEdfDac0b94175B2431Aa1A02'
bytes32 byteText = "ByteText";
}
`;

const states = await getStatesFromSourceCode(sourceCode);
expect(states).toStrictEqual([
{ slot: 0, type: 'string name;', variable: 'name' },
{ slot: 1, type: 'uint8 decimals;', variable: 'decimals' },
{ slot: 2, type: 'int256 integers;', variable: 'integers' },
{ slot: 3, type: 'bool boolean;', variable: 'boolean' },
{ slot: 4, type: 'address plainAddress;', variable: 'plainAddress' },
{
slot: 5,
type: 'address payableAddress;',
variable: 'payableAddress'
},
{ slot: 6, type: 'bytes32 byteText;', variable: 'byteText' }
])
})

test('array types', async () => {
const sourceCode = `
contract Test {
string[] public names = ["Uniswap"];
string[][] public nestedNames;
uint[] public decimalList;
}
`;

const states = await getStatesFromSourceCode(sourceCode);
expect(states).toStrictEqual([
{ slot: 0, type: 'string[] names;', variable: 'names' },
{ slot: 1, type: 'string[] nestedNames;', variable: 'nestedNames' },
{ slot: 2, type: 'uint[] decimalList;', variable: 'decimalList' }
])
})

test('mapping types', async () => {
const sourceCode = `
contract Test {
mapping (address => uint96) internal balances;
mapping (address => mapping (address => uint96)) internal allowances;
}
`;

const states = await getStatesFromSourceCode(sourceCode);
expect(states).toStrictEqual([
{
slot: 0,
type: 'mapping (address => uint96) balances;',
variable: 'balances'
},
{
slot: 1,
type: 'mapping (address => mapping (address => uint96)) allowances;',
variable: 'allowances'
}
])
})

test('user defined types', async () => {
const sourceCode = `
contract Test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
struct Checkpoint {
uint32 fromBlock;
uint96 votes;
}
Checkpoint checkpoint;
Checkpoint[] public checkpointList;
mapping(address => Checkpoint) public checkpointMap;
mapping(address => mapping(uint => Checkpoint)) public checkpointNestedMap;
struct NestedCheckpoint {
uint32 fromBlock;
SomeVotes votes;
}
struct SomeVotes {
uint96 votes;
}
mapping(address => mapping(uint => NestedCheckpoint)) public nestedCheckpointNestedMap;
struct KeyFlag {
uint key;
bool deleted;
}
struct ComplexCheckpoint {
uint32 fromBlock;
mapping(uint => SomeVotes) votes;
KeyFlag[] keys;
}
mapping(address => ComplexCheckpoint) public complexCheckpointMap;
}
`;

const states = await getStatesFromSourceCode(sourceCode);

expect(states).toStrictEqual([
{
slot: 0,
type: 'ActionChoices choice; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}',
variable: 'choice'
},
{
slot: 1,
type: 'Checkpoint checkpoint; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}',
variable: 'checkpoint'
},
{
slot: 2,
type: 'Checkpoint[] checkpointList; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}',
variable: 'checkpointList'
},
{
slot: 3,
type: 'mapping (address => Checkpoint) checkpointMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}',
variable: 'checkpointMap'
},
{
slot: 4,
type: 'mapping (address => mapping (uint => Checkpoint)) checkpointNestedMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}',
variable: 'checkpointNestedMap'
},
{
slot: 5,
type: 'mapping (address => mapping (uint => NestedCheckpoint)) nestedCheckpointNestedMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}',
variable: 'nestedCheckpointNestedMap'
},
{
slot: 6,
type: 'mapping (address => ComplexCheckpoint) complexCheckpointMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}',
variable: 'complexCheckpointMap'
}
])
})

test('constant state variables', async () => {
const sourceCode = `
contract Test {
string public name = "Uniswap";
uint public constant decimals = 18;
bool internal boolean = true;
}
`;

const states = await getStatesFromSourceCode(sourceCode);

expect(states).toStrictEqual([
{ slot: 0, type: 'string name;', variable: 'name' },
{ slot: 1, type: 'bool boolean;', variable: 'boolean' }
])
})
});
74 changes: 74 additions & 0 deletions src/utils/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { StateVariableDeclaration, StructDefinition, TypeName } from 'solidity-parser-diligence';

import State from '../models/contract/state';

const parser = require('@solidity-parser/parser'); // eslint-disable-line

const structureToSignatureType = (name: string, typeName: TypeName, structs: StructDefinition[], level = 0, isArray = false): {signature: string; type: string; hasStruct: boolean} => {
let structsDef = '';
if (level === 0 && structs && structs.length) {
for (const struct of structs) {
structsDef += ` struct ${struct.name} {` + struct.members.map(m => structureToSignatureType(m.name, m.typeName, structs, level + 1).signature).join('') + '}';
}
}

switch (typeName.type) {
case 'ElementaryTypeName':
return {
signature: `${typeName.name}${isArray ? '[]' : ''} ${name};`,
type: typeName.name,
hasStruct: false,
}
case 'ArrayTypeName': {
const res = structureToSignatureType(name, typeName.baseTypeName, structs, level + 1, true);
return {
signature: res.signature + (level === 0 && res.hasStruct ? structsDef : ''),
type: typeName.baseTypeName?.type,
hasStruct: res.hasStruct,
}
}
case 'Mapping': {
const res = structureToSignatureType(name, typeName.valueType, structs, level + 1);
return {
signature: `mapping (${typeName.keyType.name} => ${res.type}) ${name};` + (level === 0 && res.hasStruct ? structsDef : ''),
type: `mapping (${typeName.keyType.name} => ${res.type})`, // for mapping => mapping case
hasStruct: res.hasStruct,
}
}
case 'UserDefinedTypeName':
return {
signature: `${typeName.namePath}${isArray ? '[]' : ''} ${name};` + (level === 0 ? structsDef : ''),
type: typeName.namePath,
hasStruct: true,
};
}
}

export const getStatesFromSourceCode = async(sourceCode: string): Promise<State[]> => {
const ast = parser.parse(sourceCode, {
tolerant: true,
});

let list = [];
const contractDefinitions = ast?.children?.filter((item) => item.type === 'ContractDefinition');
for (const contractDefinition of contractDefinitions) {
const states = contractDefinition?.subNodes.filter(n => n.type == 'StateVariableDeclaration') as StateVariableDeclaration[];
const structs = contractDefinition?.subNodes.filter(n => n.type == 'StructDefinition') as StructDefinition[];
// TODO: Handle EnumDefinition type subnodes.

// isImmutable property of state variable is not present in type StateVariableDeclaration.
// TODO: Filter immutable variables after support added in https://github.com/ConsenSys/solidity-parser-antlr
list = list.concat(states?.filter((item) => !item.variables[0]?.isDeclaredConst)
.map((item, slot) => {
const type: string = structureToSignatureType(item.variables[0]?.name, item.variables[0]?.typeName, structs).signature;
return {
slot,
type,
variable: item.variables[0]?.name,
}
})
);
}

return list as State[];
}

0 comments on commit 25ed9c7

Please sign in to comment.