-
Notifications
You must be signed in to change notification settings - Fork 66
feat: Immutable Vault & Revenue Splitter added to aTokenVaultFactory #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…osit transfer on constructor forced it
| EXTERNAL FUNCTIONS | ||
| //////////////////////////////////////////////////////////////*/ | ||
| constructor() { | ||
| VAULT_CREATION_CODE_SSTORE2_POINTER = SSTORE2.write(LibZip.flzCompress(type(ImmutableATokenVault).creationCode)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was simply defining it as a constant (the full bytes) in this contract leading the code size limit issues?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct. The ImmutableATokenVault initcode itself has a size of 29.82 kB, so it cannot fit as a constant anywhere, I tried all the approaches I could think of, even trying to store the compressed initcode bytes (~15.6 kB) and it always exceeded the limit.
╭----------------------------------------╮
| optimizer-runs = 30_000 | current code |
|----------------------------------------+------------------+-------------------+--------------------+---------------------╮
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
+==========================================================================================================================+
| ATokenVaultFactory | 12,934 | 43,987 | 11,642 | 5,165 |
|----------------------------------------+------------------+-------------------+--------------------+---------------------|
| ImmutableATokenVault | 21,709 | 29,820 | 2,867 | 19,332 |
╰----------------------------------------+------------------+-------------------+--------------------+---------------------╯
What it does work is storing the compressed initcode bytes in some library, and getting them from an external function:
library VaultInitCodeLib {
function getCompressedInitCode() external pure returns (bytes memory) {
return hex"1c61012060405234801562000011575f80fd..."; // ~15.6k bytes - compressed initcode
}
}Then do:
LibZip.flzDecompress(VaultInitCodeLib.getCompressedInitCode())In that case we are doing a DELEGATECALL to the library instead of the EXTCODECOPY to the SSTORE2 pointer. The gas I think should be similar. I chose the SSTORE2 to avoid having the +15k raw bytes of the compressed initcode in the code, I thought that the SSTORE2 approach would be neater in that sense. But I don't mind switching to that approach if you think it would be a better one.
╭----------------------------------------╮
| optimizer-runs = 30_000 | initcode lib |
|----------------------------------------+------------------+-------------------+--------------------+---------------------╮
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
+==========================================================================================================================+
| ATokenVaultFactory | 12,959 | 12,988 | 11,617 | 36,164 |
|----------------------------------------+------------------+-------------------+--------------------+---------------------|
| ImmutableATokenVault | 21,709 | 29,820 | 2,867 | 19,332 |
|----------------------------------------+------------------+-------------------+--------------------+---------------------|
| VaultInitCodeLib | 15,891 | 15,944 | 8,685 | 33,208 |
╰----------------------------------------+------------------+-------------------+--------------------+---------------------╯
In the next comment I will post some other things I tried in case it's helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Putting the compressed initcode raw bytes directly into the factory:
╭-------------------------------------╮
| optimizer-runs = 30_000 | raw bytes |
|----------------------------------------+------------------+-------------------+--------------------+---------------------╮
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
+==========================================================================================================================+
| ATokenVaultFactory | 28,368 | 28,397 | -3,792 | 20,755 |
|----------------------------------------+------------------+-------------------+--------------------+---------------------|
| ImmutableATokenVault | 21,709 | 29,820 | 2,867 | 19,332 |
╰----------------------------------------+------------------+-------------------+--------------------+---------------------╯
If we lower the optimizer runs down to 1 (which I don't like, but just for the sake of looking at the numbers):
╭--------------------------------╮
| optimizer-runs = 1 | raw bytes |
|----------------------------------------+------------------+-------------------+--------------------+---------------------╮
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
+==========================================================================================================================+
| ATokenVaultFactory | 26,036 | 26,065 | -1,460 | 23,087 |
|----------------------------------------+------------------+-------------------+--------------------+---------------------|
| ImmutableATokenVault | 16,942 | 24,879 | 7,634 | 24,273 |
╰----------------------------------------+------------------+-------------------+--------------------+---------------------╯
That was using always the same compressed initcode that I compiled and compressed when the optimizer runs were set to 30k.
So, even if I lower the optimizer runs down to 1 for the the compressed initcode generation, the factory still exceeds the size limit:
╭------------------------------------------------------------------╮
| optimizer-runs = 1 | raw bytes generated with optimizer-runs = 1 |
|----------------------------------------+------------------+-------------------+--------------------+---------------------╮
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
+==========================================================================================================================+
| ATokenVaultFactory | 25,923 | 25,952 | -1,347 | 23,200 |
|----------------------------------------+------------------+-------------------+--------------------+---------------------|
| ImmutableATokenVault | 16,942 | 24,879 | 7,634 | 24,273 |
╰----------------------------------------+------------------+-------------------+--------------------+---------------------╯
| bytes32 salt = bytes32(_nextSalt++); | ||
|
|
||
| bytes memory vaultInitCode = abi.encodePacked( | ||
| LibZip.flzDecompress(SSTORE2.read(VAULT_CREATION_CODE_SSTORE2_POINTER)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sstore2 would be useful is this data was dynamic but since it's a constant i really don't see the need for this unless code size issues in which case i would try more to decrease size of the factory instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I explained why the SSTORE2 and InitCode Compression in this comment.
Given the size of the ImmutableATokenVault initcode, it will not fit straight away in the factory. I do not see a clear path on how to get the factory code leaner (except for getting the ATokenVaultRevenueSplitterOwner deployment to a lib, but then I'd rather take the ImmutableATokenVault to a lib as it's the bigger one in bytecode)
I think it comes down to SSTORE2+Compression (no raw bytes in the code) VS InitCodeLib+Compression (raw bytes in the code).
| function _deployVault(bytes memory vaultInitCode, bytes32 salt) internal returns (address) { | ||
| address vaultAddress; | ||
| assembly { | ||
| vaultAddress := create2(0, add(vaultInitCode, 32), mload(vaultInitCode), salt) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's just use OZ create2 abstraction for this like address computation instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why I did not use the OZ deploy function is that they always revert with "Create2: Failed on deploy" and I wanted to bubble-up the original error, for example "ZERO_INITIAL_LOCK_DEPOSIT", "EMPTY_SHARE_SYMBOL", etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we rename to immutableATokenVault
we can maintain the factory for both variants
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same for tests, is the plan to not maintain both now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan is to have a single factory. The previous factory impl was in practice also an "Immutable aToken Vault", it's just that immutability was achieved by putting a renounced proxy admin instead
| address indexed revenueSplitterOwner, | ||
| address indexed vault, | ||
| address indexed owner, | ||
| ATokenVaultRevenueSplitterOwner.Recipient[] revenueRecipients |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's reference from interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm there is no interface for this file. I can create one and use it, but given we are deploying that contract in the factory, it will not change anything in terms of things to import into the factory. Lmk
|
|
||
| console.log("Deploying aTokenVaultFactory implementation..."); | ||
|
|
||
| ATokenVaultFactory factoryImplementation = new ATokenVaultFactory(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would rm implementation suffix from this file everywhere since its not a proxy it's slight confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is meant to be used to deploy the impl so I can upgrade current factories. I can rename it anyways
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this renaming?
DeployFactory.s.sol -------------------> DeployFactoryWithProxy.s.sol
DeployFactoryImplementation.s.sol ---> DeployFactory.s.sol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can also just delete the file from the repo as I will use it only once for these upgrades
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same for tests, is the plan to not maintain both now?
| ATokenVaultRevenueSplitterOwner.Recipient[] memory revenueRecipients | ||
| ) internal returns (address) { | ||
| address revenueSplitter = address(new ATokenVaultRevenueSplitterOwner(vaultAddress, owner, revenueRecipients)); | ||
| emit RevenueSplitterOwnerDeployed(revenueSplitter, vaultAddress, owner, revenueRecipients); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't the constructor params of this contract be emitted in that contract instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deployed contract emits the relevant events, I think this is needed for the backend to make a quicker indexing, but I am happy to just emit the deployed address in the factory event and not the constructor params, cc-ing backend team to confirm @joshstevens19 @desfero
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make sure we only index well known ATokenVaultRevenueSplitterOwner contract event needs to come from an approved factory address (indexer filters if event.tx_information.address is an approved factory contract).
If the event is emitted inside the contract we can't understand quickly if it has proper shape/logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the emit RevenueSplitterOwnerDeployed(revenueSplitterAddress) vs emit RevenueSplitterOwnerDeployed(revenueSplitter, constructorArgument1, constructorArgument2, etc)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need all details to index the data without RPC lookup, otherwise indexing is 1000x slower and less predictable (network issues, RPC rate limiting, etc)
Changes to gas cost
🧾 Summary (20% most significant diffs)
Full diff report 👇
|
|
Closed by #108 given that the approach to not exceed code size limit is very unusual |
No description provided.