Skip to content

Diamond Proxy pattern based smart contracts#16

Open
udityadav-supraoracles wants to merge 15 commits intofeature/evm_automationfrom
feature/diamond_proxy_automation_registry
Open

Diamond Proxy pattern based smart contracts#16
udityadav-supraoracles wants to merge 15 commits intofeature/evm_automationfrom
feature/diamond_proxy_automation_registry

Conversation

@udityadav-supraoracles
Copy link
Copy Markdown

@udityadav-supraoracles udityadav-supraoracles commented Feb 17, 2026

This PR includes changes to accommodate use of Diamond Proxy pattern for EVM Automation Registry smart contracts.

-includes:
facets: ConfigFacet. RegistryFacet and CoreFacet.
DiamondInit: This contract is used to initialise the state of Diamond, its not added as facet. So, we don't have a flag to check for initialisation. Once Diamond is deployed, this contract is supposed to be called as part of diamondCut.

@udityadav-supraoracles udityadav-supraoracles marked this pull request as ready for review February 19, 2026 08:43
@udityadav-supraoracles
Copy link
Copy Markdown
Author

Currently the AppStorage have inner structs which doesn't allow the inner structs to be expanded later. Either we can have all variables in AppStorage struct or have a mapping pointing to inner struct as structs pointing by mapping are expandable.

Referring to: https://eip2535diamonds.substack.com/p/diamond-upgrades
Do not put structs directly in structs unless you don’t plan on ever adding more state variables to the inner structs. You won't be able to add new state variables to inner structs in upgrades without overwriting existing state variables.

Please let me know your opinion Dr @sjoshisupra @aregng.

Comment on lines +60 to +69
function setVmSigner(address _vmSigner) external {
LibDiamond.enforceIsContractOwner();

LibUtils.validateAddress(_vmSigner);

address oldVmSigner = s.vmSigner;
s.vmSigner = _vmSigner;

emit VmSignerUpdated(oldVmSigner, _vmSigner);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@udityadav-supraoracles, @aregng, I do not foresee any circumstances for VM_SIGNER to change since this is hard coded in rust layer. Is this method only to initialize after contract deployment or do we foresee this to be useful? If not, perhaps we should not have this API (if this is initialized with appropriate VM_SIGNER address at the time of deployment)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VM_SIGNER and ERC20Supra both are initialised at the time of deployment. We can remove their respective API's to update the addresses if we do not plan to change them later. Please let me know your opinion @aregng .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VM_SIGNER value will not change for sure.
Regarding ERC20Supra: currently we are deploying it as a single contract as non upgradable. If we make it upgradable for any potential upgrades in future and deploy it as a proxy contract, then I think there will not be a need to have the instance updated in scope of registry as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ERC20Supra is going to be upgradable, considering that I'm removing APIs to update ERC20Supra and VM_SIGNER.


/// @notice Function to update the ERC20Supra address.
/// @param _erc20Supra New address for ERC20Supra.
function setErc20Supra(address _erc20Supra) external {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@udityadav-supraoracles, is this to update ERC 20 contract address? Or to update the address of the registry itself where it stores ERC20 tokens collected from fee? It is not clear from the comments.
If it is the former, why does registry contract need to know ERC20 contract address?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is meant to update the address of ERC20Supra contract in registry, as the registry holds ERC20Supra address. The registry needs to know ERC20Supra's address in order to call transferFrom on it while charging fees.

Regarding fee collection, fee charged in terms of ERC20Supra tokens is stored at the registry's address(ERC20 standard contract inherited by ERC20Supra maintains mapping _balances where it stores the registry's balance).

uint256 balance = IERC20(s.erc20Supra).balanceOf(address(this));

if (balance < _amount) { revert InsufficientBalance(); }
if (balance - _amount < s.registryState.cycleLockedFees + s.registryState.totalDepositedAutomationFees) { revert RequestExceedsLockedBalance(); }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@udityadav-supraoracles, something seems off. I believe we want to withdraw from s.registryState.totalDepostedAutomationFees, meaning that shouldn't the expression be
balance - _amount < s.registryState.cycleLockedFees? does totalDepositedAutomationFees refer to fees or does it refer to the caution that we take to deter DOS via cancellation?

Copy link
Copy Markdown
Author

@udityadav-supraoracles udityadav-supraoracles Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The registry contract holds balances for cycleLockedFees and totalDepositedAutomationFees where totalDepositedAutomationFees refers to automation fee deposited by the users for their tasks during registration.

Only the amount which's been unlocked can be withdrawn. For example, lets say the contract holds 100 ERC20Supra token which comprises of cycleLockedFees and totalDepositedAutomationFees out of which 20 has been unlocked and 80 are still locked as cycleLockedFees and totalDepositedAutomationFees. This condition ensures that we are not reducing the locked cycleLockedFees and totalDepositedAutomationFees.

Comment on lines +132 to +133
if (s.registryState.gasCommittedForNextCycle > _registryMaxGasCap) { revert UnacceptableRegistryMaxGasCap(); }
if (s.registryState.sysGasCommittedForNextCycle > _sysRegistryMaxGasCap) { revert UnacceptableSysRegistryMaxGasCap(); }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@udityadav-supraoracles, shouldn't these two checks also be part of LibCommon.validateConfigParameters method? This seems like validation checks only.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It couldn't because LibCommon.validateConfigParameters is used while initialisation and updating the buffer. But in case of updating buffer the above mentioned validation is additional which is not required during initialisation.

/// @param _taskIndexes Array of task index to be processed.
function processTasks(uint64 _cycleIndex, uint64[] memory _taskIndexes) external {
// Check caller is VM Signer
if (msg.sender != s.vmSigner) { revert CallerNotVmSigner(); }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@udityadav-supraoracles, it is better to have a common method enforceIsVmSigner similar to enforceIsContractOwner.

/// @return intermediateState Returns the intermediate state.
function dropOrChargeTasks(
uint64[] memory _taskIndexes
) private returns (LibCommon.IntermediateStateOfCycleChange memory intermediateState) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this be private? I believe this need to be called by VM_SIGNER right? In fact, multiple places I believe methods are marked as private which may not work with evm and they need to be restricted to be called by VM_SIGNER only. @udityadav-supraoracles @aregng

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is called from onCycleTransition defined as internal in the same library which furthur gets called by VM_SIGNER facing function processTasks. So, there's no issue with this function being marked as private as its not directly called by VM_SIGNER.

VM_SIGNER only calls monitorCycleEnd and processTasks which're marked as external and other helper functions are marked as internal or private.

@sjoshisupra
Copy link
Copy Markdown

Currently the AppStorage have inner structs which doesn't allow the inner structs to be expanded later. Either we can have all variables in AppStorage struct or have a mapping pointing to inner struct as structs pointing by mapping are expandable.

Referring to: https://eip2535diamonds.substack.com/p/diamond-upgrades Do not put structs directly in structs unless you don’t plan on ever adding more state variables to the inner structs. You won't be able to add new state variables to inner structs in upgrades without overwriting existing state variables.

Please let me know your opinion Dr @sjoshisupra @aregng.

Hi @udityadav-supraoracles , in that case we should use the guidelines and avoid nested structs.

Comment on lines +158 to +160
for (uint8 i = 0; i < _exponent; i++) {
resultScaled = (resultScaled * baseScaled) / DECIMAL;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (uint8 i = 0; i < _exponent; i++) {
resultScaled = (resultScaled * baseScaled) / DECIMAL;
}
while (_exponent > 0) {
if (_exponent & 0x1) {
resultScaled = (resultScaled * baseScaled) / DECIMAL;
}
else {
baseScaled = baseScaled << 1;
}
_exponent = _exponent >> 1;
}

I think this is logarithmic in the value of the exponent whereas at present linear algorithm is used.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, updated with the following:

while (_exponent > 0) {
    if ((_exponent & 1) != 0) {
        resultScaled = (resultScaled * baseScaled) / DECIMAL;
     }

     _exponent >>= 1;
     if (_exponent > 0) {
         baseScaled = (baseScaled * baseScaled) / DECIMAL;
     }
}   

/// @param _taskIndex Task index to check if a task exists against it.
function ifTaskExists(uint64 _taskIndex) internal view returns (bool) {
RegistryState storage registryState = LibAppStorage.registryState();
return registryState.tasks[_taskIndex].owner != address(0) && registryState.taskIdList.contains(_taskIndex);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@udityadav-supraoracles, address(0) does not indicate a contract, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, address(0) is default value for address type.


delete registryState.tasks[_taskIndex];
require(registryState.taskIdList.remove(_taskIndex), TaskIndexNotFound());
require(registryState.userTasks[_owner].remove(_taskIndex), TaskIndexNotFound());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a system task, won't it violate this condition? @udityadav-supraoracles

Copy link
Copy Markdown
Author

@udityadav-supraoracles udityadav-supraoracles Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually registryState.userTasks keeps track of address => registered tasks. The name userTasks is confusing, it keeps track of registered tasks by an address which comprises of both UST as well as GST. Updated the name to addressToTasks.


library LibDiamond {
// 32 bytes keccak hash of a string to use as a diamond storage location.
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we modify the string passed to keccak256? It looks like this came from template and to ensure the storage at random location, we should use a unique string. @udityadav-supraoracles

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is from the standard Diamond implementation. Existing string "diamond.standard.diamond.storage" also creates a unique storage slot for our Diamond contract.

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ERC20SupraHandler is OwnableUpgradeable, UUPSUpgradeable {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my understanding is correct, then user will be using ERC20SupraHandler instead of ERC20Supra, so taking this into account I think ERC20SupraHandler should provide means to check balance, and change the allowance (increase or decrease) without minting. Otherwise user should deal with 2 contracts which will be inconvenient.

automationBaseFeeWeiPerSec: 0.001 ether,
flatRegistrationFeeWei: 0.002 ether,
taskDurationCapSecs: 3600 * 24 * 7,
registryMaxGasCap: 1_000_000,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires some more thoughts. What is the max gas at chain level for EVM? Since EVM gas cost table has not changed, I believe 1M for entire registry it too low, given that Ethereum allows 18M max gas for a single tx.

taskCapacity: 400,
cycleDurationSecs: 1200,
sysTaskDurationCapSecs: 3600 * 24 * 180,
sysRegistryMaxGasCap: 1_000_000,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here ,this could be too low for EVM

uint256 _amount
) external {
if (msg.sender != erc20SupraHandler) revert UnauthorizedCaller();
_approve(_owner, _spender, _amount);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does _approve not have any access control? Shouldn't only the owner of the account be able to give approval? What use case are we covering by giving ERC20SupraHandler the wildcard authority?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed earlier we need an API nativeToErc20SupraWithAllowance using which user can convert native to ERC20Supra while setting allowance to Automation Registry in a single transaction.

_approve is an internal function it can only be called in ERC20Supra and not ERC20SupraHandler, so I had to expose an endpoint using which nativeToErc20SupraWithAllowance in ERC20SupraHandler can set allowance during conversion.

Comment on lines +67 to +70
if (msg.value == 0) revert InvalidAmount();
IERC20Supra(erc20Supra).mint(msg.sender, msg.value);

emit NativeToERC20Supra(msg.sender, msg.value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we deduct native token from the sender?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is marked as payable, therefore user is supposed to send the native tokens along with the tx which gets deposited in the contract.

/// @notice Deposits native tokens, mints ERC20Supra tokens 1:1, and sets an allowance for a spender.
/// @param _spender The address whose allowance will be set.
/// @param _allowanceAmount The new allowance to set for the spender.
function nativeToErc20SupraWithAllowance(address _spender, uint256 _allowanceAmount) external payable {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we need this. This goes against separation of concerns.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We needed an API nativeToErc20SupraWithAllowance using which user can convert native to ERC20Supra while setting allowance to Automation Registry in a single transaction.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this taking into account separation of concerns but then user would've to execute two transactions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user I would say this is a convenience that I would like to have.

Comment on lines +79 to +84
if (_allowanceAmount == 0) revert InvalidAllowance();

IERC20Supra(erc20Supra).mint(msg.sender, msg.value);
IERC20Supra(erc20Supra).approveFor(msg.sender, _spender, _allowanceAmount);

emit NativeToERC20SupraWithAllowance(msg.sender, msg.value, _spender, _allowanceAmount);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, native token should be deducted from sender, what does validateAddress() do here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is marked as payable, therefore user is supposed to send the native tokens along with the tx which gets deposited in the contract.

validateAddress is used to validate the address of spender, the address which is granted allowance to spend. This could be Automation Registry address.

Comment on lines +96 to +97
(bool sent, ) = payable(msg.sender).call{value: _amount}("");
if (!sent) revert TransferFailed();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check that this handler contract has sufficient native balance for transfer. Also, shouldn't there be a direct transfer instead of an external call and then checking status? What's the difference between this method and doing direct native transfer?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike send and transfer, which forward only 2300 gas and can fail with contract recipients, call forwards all remaining gas and is more reliable. It also returns a bool so we can explicitly check success and revert if needed. Using call is the recommended approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants