Skip to content

Commit

Permalink
fix(pg): Handle higher storage cost on Moonbeam
Browse files Browse the repository at this point in the history
  • Loading branch information
RPate97 committed Mar 4, 2024
1 parent f86c93f commit 145ddc1
Show file tree
Hide file tree
Showing 13 changed files with 517 additions and 59 deletions.
7 changes: 7 additions & 0 deletions .changeset/good-fans-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sphinx-labs/contracts': patch
'@sphinx-labs/plugins': patch
'@sphinx-labs/core': patch
---

Handle higher storage cost on Moonbeam
8 changes: 7 additions & 1 deletion packages/contracts/contracts/foundry/Sphinx.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
GnosisSafeTransaction,
ExecutionMode,
SystemContractInfo,
ParsedAccountAccess
ParsedAccountAccess,
DeployedContractSize
} from "./SphinxPluginTypes.sol";
import { SphinxUtils } from "./SphinxUtils.sol";
import { SphinxConstants } from "./SphinxConstants.sol";
Expand Down Expand Up @@ -249,6 +250,11 @@ abstract contract Sphinx {
accesses,
safe
);

deploymentInfo.encodedDeployedContractSizes = abi.encode(
sphinxUtils.fetchDeployedContractSizes(accesses)
);

// ABI encode each `ParsedAccountAccess` element individually. If, instead, we ABI encode
// the entire array as a unit, the encoded bytes will be too large for EthersJS to ABI
// decode, which causes an error. This occurs for large deployments, i.e. greater than 50
Expand Down
12 changes: 12 additions & 0 deletions packages/contracts/contracts/foundry/SphinxPluginTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ struct ParsedAccountAccess {
Vm.AccountAccess[] nested;
}

struct DeployedContractSize {
address account;
uint size;
}

/**
* @notice Contains all of the information that's collected in a deployment on a single chain.
* The only difference between this struct and the TypeScript `DeploymentInfo` object is
Expand Down Expand Up @@ -107,6 +112,7 @@ struct FoundryDeploymentInfo {
string sphinxLibraryVersion;
bytes[] encodedAccountAccesses;
uint256[] gasEstimates;
bytes encodedDeployedContractSizes;
}

enum ExecutionMode {
Expand Down Expand Up @@ -251,4 +257,10 @@ contract SphinxPluginTypes {
view
returns (SphinxLeafWithProof[][] memory batches)
{}

function deployedContractSizesType()
external
view
returns (DeployedContractSize[] memory deployedContractSizes)
{}
}
49 changes: 48 additions & 1 deletion packages/contracts/contracts/foundry/SphinxUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
ExecutionMode,
SystemContractInfo,
GnosisSafeTransaction,
ParsedAccountAccess
ParsedAccountAccess,
DeployedContractSize
} from "./SphinxPluginTypes.sol";
import { SphinxConstants } from "./SphinxConstants.sol";
import { ICreateCall } from "./interfaces/ICreateCall.sol";
Expand Down Expand Up @@ -926,6 +927,12 @@ contract SphinxUtils is SphinxConstants, StdUtils {
"encodedAccountAccesses",
_deployment.encodedAccountAccesses
);
vm.serializeBytes(
deploymentInfoKey,
"encodedDeployedContractSizes",
_deployment.encodedDeployedContractSizes
);

// Next, we'll serialize `uint` values as ABI encoded bytes. We don't serialize them as
// numbers to prevent the possibility that they lose precision due JavaScript's relatively
// low integer size limit. We'll ABI decode these values in TypeScript. It'd be simpler to
Expand Down Expand Up @@ -1011,6 +1018,46 @@ contract SphinxUtils is SphinxConstants, StdUtils {
return finalJson;
}

function fetchNumCreateAccesses(
Vm.AccountAccess[] memory _accesses
) public pure returns (uint) {
uint numCreateAccesses = 0;
for (uint i = 0; i < _accesses.length; i++) {
if (_accesses[i].kind == VmSafe.AccountAccessKind.Create) {
numCreateAccesses += 1;
}
}
return numCreateAccesses;
}

function fetchDeployedContractSizes(
Vm.AccountAccess[] memory _accesses
) public view returns (DeployedContractSize[] memory) {
uint numCreateAccesses = fetchNumCreateAccesses(_accesses);
DeployedContractSize[] memory deployedContractSizes = new DeployedContractSize[](
numCreateAccesses
);
uint deployContractSizeIndex = 0;
for (uint i = 0; i < _accesses.length; i++) {
if (_accesses[i].kind == VmSafe.AccountAccessKind.Create) {
// We could also read the size of the code from the AccountAccess deployedCode field
// We don't do that because Foundry occasionally does not populate that field when
// it should.
address account = _accesses[i].account;
uint size;
assembly {
size := extcodesize(account)
}
deployedContractSizes[deployContractSizeIndex] = DeployedContractSize(
account,
size
);
deployContractSizeIndex += 1;
}
}
return deployedContractSizes;
}

function parseAccountAccesses(
Vm.AccountAccess[] memory _accesses,
address _safeAddress
Expand Down
132 changes: 129 additions & 3 deletions packages/contracts/src/networks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,121 @@
import {
AccountAccess,
AccountAccessKind,
DeployedContractSize,
ParsedAccountAccess,
} from './types'

/**
* Calculates the storage used by a single AccountAccess. This function is intentionally very simple.
* We naively assume that every write takes up a full 32 bytes of storage despite the fact that there
* are many cases where the storage usage is less or even negative. We do this because we prefer to
* always overestimate the cost by a reasonable amount. Since the block gas limit on Moonbeam and
* related networks is comfortably high (15 million), this does not impede the users ability to deploy
* large contracts.
*/
const calculateStorageSizeForAccountAccess = (
access: AccountAccess,
deployedContractSizes: DeployedContractSize[]
): { contractStorageSize: number; writeStorageSize: number } => {
const storageWriteSize =
access.storageAccesses.filter((storageAccess) => storageAccess.isWrite)
.length * 32

if (access.kind === AccountAccessKind.Create) {
const deployedContractSize = deployedContractSizes.find(
(deployedContact) => deployedContact.account === access.account
)

if (!deployedContractSize) {
throw new Error(
'Failed to find deployed contract size. This is a bug, please report it to the developers.'
)
}

return {
writeStorageSize: storageWriteSize,
contractStorageSize: Number(deployedContractSize.size),
}
} else {
return {
writeStorageSize: storageWriteSize,
contractStorageSize: 0,
}
}
}

/**
* Calculates the cost of a transaction on Moonbeam using their higher gas cost per byte.
*
* @param baseGas The estimated gas cost according to Foundry.
* @param deployedContractSizes The sizes of any contracts deployed during this transaction.
* @param access The ParsedAccountAccess for the transaction.
* @returns
*/
export const calculateActionLeafGasForMoonbeam = (
foundryGas: string,
deployedContractSizes: DeployedContractSize[],
access: ParsedAccountAccess
): string => {
// Fetch the storage used by the root account access
const { contractStorageSize, writeStorageSize } =
calculateStorageSizeForAccountAccess(access.root, deployedContractSizes)

// Fetch the storage used by all the nested accesses
const nestedStorageSizes = access.nested.map((nestedAccountAccess) =>
calculateStorageSizeForAccountAccess(
nestedAccountAccess,
deployedContractSizes
)
)
const nestedContractStorageSize = nestedStorageSizes
.map((storageSize) => storageSize.contractStorageSize)
.reduce((prev, curr) => prev + curr, 0)
const nestedWriteStorageSize = nestedStorageSizes
.map((storageSize) => storageSize.writeStorageSize)
.reduce((prev, curr) => prev + curr, 0)

// Calculate the total storage for the full transaction
const totalContractSize = contractStorageSize + nestedContractStorageSize
const totalWriteStorageSize = writeStorageSize + nestedWriteStorageSize

// Gas per byte ratio = Block Gas Limit / (Block Storage Limit (kb) * 1024 Bytes)
const ratio = 15_000_000 / (40 * 1024)

// Total gas cost for storage on moonbeam
// The ratio isn't an exact integer, so we round the result up
const moonbeamStorageCost = Math.ceil(
(totalContractSize + totalWriteStorageSize) * ratio
)

/**
* Calculate the cost of the transaction according to Foundry without gas costs related to
* storage of the contracts.
*
* If we did not subtract the 200 gas / byte here, then we would wildly overestimate the cost of
* deploying contracts which could prevent the user from being able to deploy even reasonably
* sized contracts on Moonbeam due to their lower block gas limit.
*
* It's worth noting that the `foundryGas` value includes some value for the intrinsic storage
* cost of any other storage writes (i.e SSTOREs) in the transaction. Since we do not subtract
* the cost of that storage here as well, we are double counting the cost of standard storage.
* We do not implement logic to handle this because Ethereum does not have a straightforward
* way to calculate the intrinsic cost of storage. It can vary meaningfully depending on the
* specific situation. So we chose to not subtract anything for those writes since it's safer.
*
* This does introduce an edge case where if the user has a transaction that includes a very
* large number of writes (i.e 1000 SSTORES), we would likely wildly overestimate the cost of
* that transaction. The user would still be able to execute it through us, but since we're
* overestimating the cost, the user would be limited in the amount of storage they can write
* in a single transaction.
*/
const baseGasLessStorageCost = Number(foundryGas) - 200 * totalContractSize

// The final cost is the base cost without any cost for storage, plus
// the higher cost for storage on Moonbeam
return (baseGasLessStorageCost + moonbeamStorageCost).toString()
}

export type SupportedNetwork = {
name: string
displayName: string
Expand All @@ -24,6 +142,11 @@ export type SupportedNetwork = {
provider: RollupProvider
type: RollupType
}
handleNetworkSpecificMerkleLeafGas?: (
foundryGas: string,
deployedContractSizes: DeployedContractSize[],
access: ParsedAccountAccess
) => string
}

export type SupportedLocalNetwork = {
Expand Down Expand Up @@ -642,8 +765,9 @@ export const SPHINX_NETWORKS: Array<SupportedNetwork> = [
decimals: 18,
legacyTx: false,
actionGasLimitBuffer: false,
useHigherMaxGasLimit: false,
useHigherMaxGasLimit: true,
eip2028: true,
handleNetworkSpecificMerkleLeafGas: calculateActionLeafGasForMoonbeam,
},
{
name: 'moonbeam',
Expand All @@ -665,8 +789,9 @@ export const SPHINX_NETWORKS: Array<SupportedNetwork> = [
decimals: 18,
legacyTx: false,
actionGasLimitBuffer: false,
useHigherMaxGasLimit: false,
useHigherMaxGasLimit: true,
eip2028: true,
handleNetworkSpecificMerkleLeafGas: calculateActionLeafGasForMoonbeam,
},
{
name: 'moonbase_alpha',
Expand All @@ -688,8 +813,9 @@ export const SPHINX_NETWORKS: Array<SupportedNetwork> = [
decimals: 18,
legacyTx: false,
actionGasLimitBuffer: false,
useHigherMaxGasLimit: false,
useHigherMaxGasLimit: true,
eip2028: true,
handleNetworkSpecificMerkleLeafGas: calculateActionLeafGasForMoonbeam,
},
{
name: 'fuse',
Expand Down
49 changes: 49 additions & 0 deletions packages/contracts/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
export enum AccountAccessKind {
Call = '0',
DelegateCall = '1',
CallCode = '2',
StaticCall = '3',
Create = '4',
SelfDestruct = '5',
Resume = '6',
Balance = '7',
Extcodesize = '8',
Extcodehash = '9',
Extcodecopy = '10',
}

export type AccountAccess = {
chainInfo: {
forkId: string
chainId: string
}
kind: AccountAccessKind
account: string
accessor: string
initialized: boolean
oldBalance: string
newBalance: string
deployedCode: string
value: string
data: string
reverted: boolean
storageAccesses: Array<{
account: string
slot: string
isWrite: boolean
previousValue: string
newValue: string
reverted: boolean
}>
}

export type ParsedAccountAccess = {
root: AccountAccess
nested: Array<AccountAccess>
}

export type DeployedContractSize = {
account: string
size: string
}

export type DecodedApproveLeafData = {
safeProxy: string
moduleProxy: string
Expand Down
Loading

0 comments on commit 145ddc1

Please sign in to comment.