Skip to content

Commit

Permalink
fix(pg): throw an error if contracts are above the size limit
Browse files Browse the repository at this point in the history
  • Loading branch information
sam-goldman committed Apr 7, 2024
1 parent c7af7ef commit 52ae5f1
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/curly-rules-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sphinx-labs/plugins': patch
'@sphinx-labs/core': patch
---

Throw an error if contracts are above the size limit
7 changes: 7 additions & 0 deletions packages/contracts/contracts/foundry/Sphinx.sol
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ abstract contract Sphinx {
// the delegatecall in our error message.
require(success, "Sphinx: Deployment script failed.");
Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff();
// Set the `deployedCode` field for each AccountAccess of kind `Create`. There may be a bug
// in Foundry that causes this field to not always be populated.
for (uint256 i = 0; i < accesses.length; i++) {
if (accesses[i].kind == VmSafe.AccountAccessKind.Create) {
accesses[i].deployedCode = accesses[i].account.code;
}
}

// We have to copy fundsRequestedForSafe and safeStartingBalance into deploymentInfo before
// calling vm.revertTo because that cheatcode will clear the state variables.
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ export const EXECUTION_LOCK_TIME = 15 * 60

export const RECOMMENDED_REMAPPING =
'@sphinx-labs/contracts/=lib/sphinx/packages/contracts/contracts/foundry'

export const MAX_CONTRACT_SIZE_LIMIT = 24576
10 changes: 10 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
recursivelyConvertResult,
DETERMINISTIC_DEPLOYMENT_PROXY_ADDRESS,
CreateCallArtifact,
MAX_CONTRACT_SIZE_LIMIT,
} from '@sphinx-labs/contracts'

import {
Expand Down Expand Up @@ -1709,4 +1710,13 @@ export const isPublicAsyncMethod = (
return false
}

/**
* Returns true if the given bytecode exceeds the contract size size limit as defined by:
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md
*/
export const exceedsContractSizeLimit = (deployedBytecode: string): boolean => {
const bytesLength = remove0x(deployedBytecode).length / 2
return bytesLength > MAX_CONTRACT_SIZE_LIMIT
}

export const sphinxCoreUtils = { sleep, callWithTimeout, isPublicAsyncMethod }
3 changes: 3 additions & 0 deletions packages/plugins/contracts/test/MyContracts.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { Governor } from "@openzeppelin/contracts/governance/Governor.sol";
import { SphinxUtils } from "@sphinx-labs/contracts/contracts/foundry/SphinxUtils.sol";

struct TopLevelStruct {
int256 a;
Expand Down Expand Up @@ -200,3 +201,5 @@ contract MyImmutableContract {
return myFirstImmutable + uint256(mySecondImmutable);
}
}

contract ExceedsContractMaxSizeLimit is SphinxUtils {}
6 changes: 6 additions & 0 deletions packages/plugins/src/cli/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
getSphinxConfigFromScript,
parseScriptFunctionCalldata,
readInterface,
assertContractSizeLimitNotExceeded,
writeSystemContracts,
} from '../foundry/utils'
import { getFoundryToml } from '../foundry/options'
Expand Down Expand Up @@ -304,6 +305,11 @@ export const deploy = async (
initCodeWithArgsArray
)

assertContractSizeLimitNotExceeded(
deploymentInfo.accountAccesses,
configArtifacts
)

await sphinxContext.assertNoLinkedLibraries(
scriptPath,
foundryToml.cachePath,
Expand Down
8 changes: 8 additions & 0 deletions packages/plugins/src/cli/propose/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
assertValidVersions,
validateProposalNetworks,
parseScriptFunctionCalldata,
assertContractSizeLimitNotExceeded,
} from '../../foundry/utils'
import { SphinxContext } from '../context'
import { FoundryToml } from '../../foundry/types'
Expand Down Expand Up @@ -177,6 +178,13 @@ export const buildNetworkConfigArray: BuildNetworkConfigArray = async (
initCodeWithArgsArray
)

collected.forEach(({ deploymentInfo }) =>
assertContractSizeLimitNotExceeded(
deploymentInfo.accountAccesses,
configArtifacts
)
)

await sphinxContext.assertNoLinkedLibraries(
scriptPath,
foundryToml.cachePath,
Expand Down
22 changes: 22 additions & 0 deletions packages/plugins/src/error-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const contractsExceedSizeLimitErrorMessage = (
contracts: Array<{ address: string; fullyQualifiedName?: string }>
): string => {
const contractLines = contracts
.map((contract) => {
let line = `- `
if (contract.fullyQualifiedName) {
line += `${contract.fullyQualifiedName} at `
}
line += `${contract.address}`
if (!contract.fullyQualifiedName) {
line += ' (unlabeled)'
}
return line
})
.join('\n')

return (
`The following contracts are over the contract size limit (24,576 bytes), which means they\n` +
`cannot be deployed on live networks:\n${contractLines}`
)
}
35 changes: 35 additions & 0 deletions packages/plugins/src/foundry/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@sphinx-labs/core/dist/languages/solidity/types'
import {
decodeDeterministicDeploymentProxyData,
exceedsContractSizeLimit,
execAsync,
fetchNetworkConfigFromDeploymentConfig,
formatSolcLongVersion,
Expand Down Expand Up @@ -101,6 +102,7 @@ import {
getMixedNetworkTypeErrorMessage,
getUnsupportedNetworkErrorMessage,
} from '../error-messages'
import { contractsExceedSizeLimitErrorMessage } from '../../error-messages'

const readFileAsync = promisify(readFile)

Expand Down Expand Up @@ -1772,3 +1774,36 @@ export const validateProposalNetworks = async (
const rpcUrls = valid.map(({ rpcUrl }) => rpcUrl)
return { rpcUrls, isTestnet }
}

export const assertContractSizeLimitNotExceeded = (
accountAccesses: Array<ParsedAccountAccess>,
configArtifacts: ConfigArtifacts
): void => {
const flat = accountAccesses.flatMap((access) => [
access.root,
...access.nested,
])

const tooLarge: Array<{ address: string; fullyQualifiedName?: string }> = []

for (const access of flat) {
if (
access.kind === AccountAccessKind.Create &&
exceedsContractSizeLimit(access.deployedCode)
) {
const initCodeWithArgs = access.data
// Find the fully qualified name if it exists. This call returns `undefined` if the contract
// is unlabeled.
const fullyQualifiedName = findFullyQualifiedNameForInitCode(
initCodeWithArgs,
configArtifacts
)

tooLarge.push({ address: access.account, fullyQualifiedName })
}
}

if (tooLarge.length > 0) {
throw new Error(contractsExceedSizeLimitErrorMessage(tooLarge))
}
}
27 changes: 26 additions & 1 deletion packages/plugins/test/mocha/dummy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Operation, SphinxMerkleTree } from '@sphinx-labs/contracts'
import {
AccountAccess,
AccountAccessKind,
Operation,
SphinxMerkleTree,
} from '@sphinx-labs/contracts'
import {
ActionInputType,
BuildInfo,
Expand Down Expand Up @@ -340,3 +345,23 @@ export const getDummyDeployment = (): Deployment => {
treeSigners: [],
}
}

export const getDummyAccountAccess = (): AccountAccess => {
return {
chainInfo: {
forkId: '0x1',
chainId: '0x3',
},
kind: AccountAccessKind.Balance,
account: '0xAccount',
accessor: '0xAccessor',
initialized: true,
oldBalance: '1000',
newBalance: '1500',
deployedCode: '0xCode',
value: '500',
data: '0xData',
reverted: false,
storageAccesses: [],
}
}
25 changes: 25 additions & 0 deletions packages/plugins/test/mocha/error-messages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect } from 'chai'

import { contractsExceedSizeLimitErrorMessage } from '../../src/error-messages'

describe('Error Messages', () => {
it('contractsExceedSizeLimitErrorMessage', () => {
const contracts = [
{
address: '0xABCDEF1234567890ABCDEF1234567890ABCDEF12',
fullyQualifiedName: 'contracts/ExampleContract.sol:ExampleContract',
},
{ address: '0x1234567890ABCDEF1234567890ABCDEF12345678' }, // Unlabeled
]
// eslint-disable-next-line prettier/prettier
const expectedMessage =
`The following contracts are over the contract size limit (24,576 bytes), which means they
cannot be deployed on live networks:
- contracts/ExampleContract.sol:ExampleContract at 0xABCDEF1234567890ABCDEF1234567890ABCDEF12
- 0x1234567890ABCDEF1234567890ABCDEF12345678 (unlabeled)`

const actualMessage = contractsExceedSizeLimitErrorMessage(contracts)

expect(actualMessage).to.equal(expectedMessage)
})
})
22 changes: 22 additions & 0 deletions packages/plugins/test/mocha/fake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import {
SphinxTransactionReceipt,
} from '@sphinx-labs/core'
import {
AccountAccessKind,
ContractArtifact,
Operation,
ParsedAccountAccess,
SphinxModuleABI,
} from '@sphinx-labs/contracts'
import { EventFragment, ethers } from 'ethers'

import {
dummyBuildInfoId,
dummyModuleAddress,
getDummyAccountAccess,
getDummyBuildInfos,
getDummyCompilerInput,
getDummyEventLog,
Expand Down Expand Up @@ -145,3 +148,22 @@ export const getFakeDeploymentConfig = async (
version: '0',
}
}

export const getFakeParsedAccountAccess = (fields: {
kind: AccountAccessKind
data: string
account: string
deployedCode: string
}): ParsedAccountAccess => {
const { kind, data, account, deployedCode } = fields
const root = getDummyAccountAccess()
root.kind = kind
root.data = data
root.account = account
root.deployedCode = deployedCode

return {
root,
nested: [],
}
}
76 changes: 76 additions & 0 deletions packages/plugins/test/mocha/foundry/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { ConstructorFragment, ethers } from 'ethers'
import {
AccountAccessKind,
ContractArtifact,
LinkReferences,
MAX_CONTRACT_SIZE_LIMIT,
parseFoundryContractArtifact,
remove0x,
} from '@sphinx-labs/contracts'
Expand All @@ -33,6 +35,7 @@ import * as MyContract2Artifact from '../../../out/artifacts/MyContracts.sol/MyC
import * as MyContractWithLibrariesArtifact from '../../../out/artifacts/MyContracts.sol/MyContractWithLibraries.json'
import * as MyImmutableContractArtifact from '../../../out/artifacts/MyContracts.sol/MyImmutableContract.json'
import * as MyLargeContractArtifact from '../../../out/artifacts/MyContracts.sol/MyLargeContract.json'
import * as ExceedsContractMaxSizeLimitArtifact from '../../../out/artifacts/MyContracts.sol/ExceedsContractMaxSizeLimit.json'
import {
encodeFunctionCalldata,
getAnvilRpcUrl,
Expand All @@ -43,6 +46,7 @@ import {
} from '../common'
import { FoundryToml } from '../../../src/foundry/types'
import {
assertContractSizeLimitNotExceeded,
assertNoLinkedLibraries,
makeGetConfigArtifacts,
parseScriptFunctionCalldata,
Expand All @@ -59,6 +63,9 @@ import {
getMixedNetworkTypeErrorMessage,
getUnsupportedNetworkErrorMessage,
} from '../../../src/foundry/error-messages'
import { contractsExceedSizeLimitErrorMessage } from '../../../src/error-messages'
import { dummyUnlabeledAddress } from '../dummy'
import { getFakeConfigArtifacts, getFakeParsedAccountAccess } from '../fake'

describe('Utils', async () => {
let foundryToml: FoundryToml
Expand Down Expand Up @@ -1188,4 +1195,73 @@ describe('Utils', async () => {
expect(actualCalldata).to.equal(calldata)
})
})

describe('assertContractSizeLimitNotExceeded', () => {
it('does not throw an error when contract size is less than the size limit', async () => {
const artifact = parseFoundryContractArtifact(MyContract2Artifact)
const address = makeAddress(1)
const fullyQualifiedName = `${artifact.sourceName}:${artifact.contractName}`

const accesses = [
getFakeParsedAccountAccess({
kind: AccountAccessKind.Create,
data: artifact.bytecode,
account: address,
deployedCode: artifact.deployedBytecode,
}),
]
const configArtifacts = await getFakeConfigArtifacts(
[fullyQualifiedName],
foundryToml.artifactFolder
)

expect(() =>
assertContractSizeLimitNotExceeded(accesses, configArtifacts)
).to.not.throw
})

it('throws an error when a labeled and unlabeled contract exceed the size limit', async () => {
const exceedsMaxSizeAddress = '0x' + '11'.repeat(20)
const exceedsMaxSizeArtifact = parseFoundryContractArtifact(
ExceedsContractMaxSizeLimitArtifact
)
const exceedsMaxSizeFullyQualifiedName = `${exceedsMaxSizeArtifact.sourceName}:${exceedsMaxSizeArtifact.contractName}`

const expectedErrorMessageInput = [
{
address: dummyUnlabeledAddress,
},
{
address: exceedsMaxSizeAddress,
fullyQualifiedName: exceedsMaxSizeFullyQualifiedName,
},
]

const accesses = [
getFakeParsedAccountAccess({
kind: AccountAccessKind.Create,
data: '', // Empty so that the initCodeWithArgs doesn't match a labeled contract
account: dummyUnlabeledAddress,
deployedCode: '0x' + '00'.repeat(MAX_CONTRACT_SIZE_LIMIT + 1),
}),
getFakeParsedAccountAccess({
kind: AccountAccessKind.Create,
data: ExceedsContractMaxSizeLimitArtifact.bytecode.object,
account: exceedsMaxSizeAddress,
deployedCode:
ExceedsContractMaxSizeLimitArtifact.deployedBytecode.object,
}),
]
const configArtifacts = await getFakeConfigArtifacts(
[exceedsMaxSizeFullyQualifiedName],
foundryToml.artifactFolder
)

expect(() =>
assertContractSizeLimitNotExceeded(accesses, configArtifacts)
).to.throw(
contractsExceedSizeLimitErrorMessage(expectedErrorMessageInput)
)
})
})
})

0 comments on commit 52ae5f1

Please sign in to comment.