Skip to content
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

feat: factory skeleton #66

Merged
merged 26 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3da9f3c
feat: factory skeleton
petrovska-petro Jun 24, 2024
1d16f99
feat: introduce role of factory in the virtual module & constructor
petrovska-petro Jun 24, 2024
7b53e44
merge latest content of branch feat/cl-migration
petrovska-petro Jun 24, 2024
e0361ef
feat: adapt to submodule grabbing
petrovska-petro Jun 24, 2024
4067329
feat: add infinite approval for smooth ops for LINK in constructor
petrovska-petro Jun 24, 2024
fe207de
test: adapt base fixture towards new flow that factory deploys new vi…
petrovska-petro Jun 24, 2024
a474d2b
Merge branch 'main' into feat/factory
gosuto-inzasheru Jun 24, 2024
7d0fe14
ci: run `forge t` first to get exit code
gosuto-inzasheru Jun 24, 2024
c5963bf
feat: make the factory storage variable public
petrovska-petro Jun 26, 2024
946cfaa
feat: make it actually immutable variable `FACTORY`
petrovska-petro Jun 26, 2024
9b68b85
fix: use proper naming for the modifier
petrovska-petro Jun 26, 2024
78b75d6
Update test/BaseFixture.sol
petrovska-petro Jun 26, 2024
17bdb02
fix: consistency on `rolesModule` everywhere
petrovska-petro Jun 26, 2024
f2b16b8
fix: follow english order of terminology Neither-Nor
petrovska-petro Jun 26, 2024
bf508c4
test: fix a revert error towards new expected error type
petrovska-petro Jun 26, 2024
cc7ad57
fix: not needed anymore to run twice
gosuto-inzasheru Jun 27, 2024
133073c
Update src/RoboSaverVirtualModuleFactory.sol
petrovska-petro Jun 28, 2024
26f9514
feat: reorg files with data types in its own directory
petrovska-petro Jun 28, 2024
095e0fc
feat: verify the delay & roles module matches with the caller
petrovska-petro Jun 28, 2024
c792360
test: add hit on Factory constructor
petrovska-petro Jun 28, 2024
d3ea943
test: cover reverts cases related to verification
petrovska-petro Jun 28, 2024
8f19978
test: commence structure for `test_RevertWhen_UpkeepReturnsZero`
petrovska-petro Jun 28, 2024
96f6762
natspec: RoboSaverVirtualModuleFactory
petrovska-petro Jun 28, 2024
c57a5fb
test: make codecov exclude script file
gosuto-inzasheru Jun 28, 2024
03d92fa
test: exclude all scripts via codecov config
gosuto-inzasheru Jun 28, 2024
21ec122
test: also ignore `test/` for codecov reports
gosuto-inzasheru Jun 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
forge build
- name: Run Forge tests
run: |
forge coverage --report lcov -vvv
forge test && forge coverage --report lcov -vvv
- name: Upload coverage report to Codecov
uses: "codecov/codecov-action@v4"
with:
Expand Down
18 changes: 13 additions & 5 deletions script/GnosisPayInfraDeployment.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {Delay} from "@delay-module/Delay.sol";
import {Roles} from "@roles-module/Roles.sol";
import {Bouncer} from "@gnosispay-kit/Bouncer.sol";

import {RoboSaverVirtualModuleFactory} from "../src/RoboSaverVirtualModuleFactory.sol";
import {RoboSaverVirtualModule} from "../src/RoboSaverVirtualModule.sol";

/// @notice Deploys a setup mirroring GnosisPay infrastructure components and RoboSaverVirtualModule, in the following order:
/// 1. {RolesModule}
/// 2. {DelayModule}
/// 3. {Bouncer}
/// 4. {RoboSaverVirtualModule}
/// 4. {RoboSaverVirtualModuleFactory}
/// 5. {RoboSaverVirtualModule}
contract GnosisPayInfraDeployment is Script {
// safe target
address constant GNOSIS_SAFE = 0xa4A4a4879dCD3289312884e9eC74Ed37f9a92a55;
Expand Down Expand Up @@ -43,7 +45,8 @@ contract GnosisPayInfraDeployment is Script {

Bouncer bouncerContract;

// robosaver module
// robosaver module & factory
RoboSaverVirtualModuleFactory roboModuleFactory;
RoboSaverVirtualModule roboModule;

function run() public {
Expand All @@ -61,10 +64,15 @@ contract GnosisPayInfraDeployment is Script {
// 3. {Bouncer}
bouncerContract = new Bouncer(GNOSIS_SAFE, address(rolesModule), SET_ALLOWANCE_SELECTOR);

// 4. {RoboSaverVirtualModule}
roboModule = new RoboSaverVirtualModule(address(delayModule), address(rolesModule), 50e18, 200);
// 4. {RoboSaverVirtualModuleFactory}
roboModuleFactory = new RoboSaverVirtualModuleFactory();

// 5. {Allowance config}
// 5. {RoboSaverVirtualModule}
roboModule = new RoboSaverVirtualModule(
address(roboModuleFactory), address(delayModule), address(rolesModule), 50e18, 200
);

// 6. {Allowance config}
rolesModule.setAllowance(
SET_ALLOWANCE_KEY,
MIN_EURE_ALLOWANCE,
Expand Down
13 changes: 11 additions & 2 deletions src/RoboSaverVirtualModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ contract RoboSaverVirtualModule is
/*//////////////////////////////////////////////////////////////////////////
PUBLIC STORAGE
//////////////////////////////////////////////////////////////////////////*/
address factory;
gosuto-inzasheru marked this conversation as resolved.
Show resolved Hide resolved

IDelayModifier public delayModule;
IRolesModifier public rolesModule;
Expand Down Expand Up @@ -149,6 +150,7 @@ contract RoboSaverVirtualModule is

error NotKeeper(address agent);
error NotAdmin(address agent);
error NorAdminNeitherFactory(address agent);
gosuto-inzasheru marked this conversation as resolved.
Show resolved Hide resolved

error ZeroAddressValue();
error ZeroUintValue();
Expand All @@ -174,11 +176,18 @@ contract RoboSaverVirtualModule is
_;
}

/// @notice Enforce that the function is called by the admin or the factory only
modifier onlyAdminAndFactory() {
gosuto-inzasheru marked this conversation as resolved.
Show resolved Hide resolved
if (msg.sender != CARD && msg.sender != factory) revert NorAdminNeitherFactory(msg.sender);
_;
}

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/

constructor(address _delayModule, address _rolesModule, uint256 _buffer, uint16 _slippage) {
constructor(address _factory, address _delayModule, address _rolesModule, uint256 _buffer, uint16 _slippage) {
factory = _factory;
delayModule = IDelayModifier(_delayModule);
rolesModule = IRolesModifier(_rolesModule);
buffer = _buffer;
Expand Down Expand Up @@ -223,7 +232,7 @@ contract RoboSaverVirtualModule is

/// @notice Assigns a new keeper address
/// @param _keeper The address of the new keeper
function setKeeper(address _keeper) external onlyAdmin {
function setKeeper(address _keeper) external onlyAdminAndFactory {
if (_keeper == address(0)) revert ZeroAddressValue();

address oldKeeper = keeper;
Expand Down
124 changes: 124 additions & 0 deletions src/RoboSaverVirtualModuleFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {IERC20} from "@chainlink/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {IKeeperRegistryMaster} from "@chainlink/automation/interfaces/v2_1/IKeeperRegistryMaster.sol";
import {IKeeperRegistrar} from "./interfaces/chainlink/IKeeperRegistrar.sol";

import {RoboSaverVirtualModule} from "./RoboSaverVirtualModule.sol";

contract RoboSaverVirtualModuleFactory {
/*//////////////////////////////////////////////////////////////////////////
DATA TYPES
//////////////////////////////////////////////////////////////////////////*/
struct VirtualModuleDetails {
address virtualModuleAddress;
uint256 upkeepId;
}

/*//////////////////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////////////////*/

IERC20 constant LINK = IERC20(0xE2e73A1c69ecF83F464EFCE6A5be353a37cA09b2);
IKeeperRegistryMaster constant CL_REGISTRY = IKeeperRegistryMaster(0x299c92a219F61a82E91d2062A262f7157F155AC1);
IKeeperRegistrar constant CL_REGISTRAR = IKeeperRegistrar(0x0F7E163446AAb41DB5375AbdeE2c3eCC56D9aA32);

/*//////////////////////////////////////////////////////////////////////////
PUBLIC STORAGE
//////////////////////////////////////////////////////////////////////////*/

// card -> (module address, upkeep id)
mapping(address => VirtualModuleDetails) public virtualModules;

/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/
event RoboSaverVirtualModuleCreated(address virtualModule, address card, uint256 upkeepId, uint256 timestamp);

/*//////////////////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////////////////*/
error UpkeepZero();

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/

constructor() {
LINK.approve(address(CL_REGISTRAR), type(uint256).max);
}

/*//////////////////////////////////////////////////////////////////////////
EXTERNAL METHODS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Creates a virtual module for a card
/// @param _delayModule The address of the delay module
/// @param _roleModule The address of the role module
/// @param _buffer The buffer for the virtual module (configurable)
/// @param _slippage The slippage for the virtual module (configurable)
function createVirtualModule(address _delayModule, address _roleModule, uint256 _buffer, uint16 _slippage)
gosuto-inzasheru marked this conversation as resolved.
Show resolved Hide resolved
external
{
// @todo sanity checks on the inputs!

// uses `CARD` address has to helps pre-determining the address of the virtual module given the salt
gosuto-inzasheru marked this conversation as resolved.
Show resolved Hide resolved
petrovska-petro marked this conversation as resolved.
Show resolved Hide resolved
address virtualModule = address(
new RoboSaverVirtualModule{salt: keccak256(abi.encodePacked(msg.sender))}(
address(this), _delayModule, _roleModule, _buffer, _slippage
)
);

uint256 upkeepId = _registerRoboSaverVirtualModule(virtualModule);

// extracts forwarder address & sets keeper
address keeper = CL_REGISTRY.getForwarder(upkeepId);
RoboSaverVirtualModule(virtualModule).setKeeper(keeper);

emit RoboSaverVirtualModuleCreated(virtualModule, msg.sender, upkeepId, block.timestamp);
}

/*//////////////////////////////////////////////////////////////////////////
INTERNAL METHODS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Registers the virtual module in the Chainlink Keeper Registry
/// @param _virtualModule The address of the virtual module to be registered
function _registerRoboSaverVirtualModule(address _virtualModule) internal returns (uint256 upkeepId_) {
IKeeperRegistrar.RegistrationParams memory registrationParams = IKeeperRegistrar.RegistrationParams({
name: string.concat(RoboSaverVirtualModule(_virtualModule).name(), "-", _addressToString(msg.sender)),
encryptedEmail: "",
upkeepContract: _virtualModule,
gasLimit: 2_000_000, // @todo optimise from the gas logs the right value. perhaps we are overshooting
gosuto-inzasheru marked this conversation as resolved.
Show resolved Hide resolved
adminAddress: address(this), // @note the factory is the admin
triggerType: 0,
checkData: "",
triggerConfig: "",
offchainConfig: "",
amount: 200e18 // @note dummy value for now
});

upkeepId_ = CL_REGISTRAR.registerUpkeep(registrationParams);
if (upkeepId_ == 0) revert UpkeepZero();

virtualModules[msg.sender] = VirtualModuleDetails({virtualModuleAddress: _virtualModule, upkeepId: upkeepId_});
}

/// @notice Converts an address to a string
function _addressToString(address _addr) internal pure returns (string memory) {
gosuto-inzasheru marked this conversation as resolved.
Show resolved Hide resolved
bytes32 value = bytes32(uint256(uint160(_addr)));
bytes memory alphabet = "0123456789abcdef";

bytes memory str = new bytes(42);
str[0] = "0";
str[1] = "x";

for (uint256 i = 0; i < 20; i++) {
str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)];
str[3 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)];
}

return string(str);
}
}
47 changes: 22 additions & 25 deletions test/BaseFixture.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import "@balancer-v2/interfaces/contracts/pool-stable/StablePoolUserData.sol";
import {IKeeperRegistryMaster} from "@chainlink/automation/interfaces/v2_1/IKeeperRegistryMaster.sol";
import {IKeeperRegistrar} from "../src/interfaces/chainlink/IKeeperRegistrar.sol";

import {RoboSaverVirtualModuleFactory} from "../src/RoboSaverVirtualModuleFactory.sol";
import {RoboSaverVirtualModule} from "../src/RoboSaverVirtualModule.sol";

import {ISafeProxyFactory} from "@gnosispay-kit/interfaces/ISafeProxyFactory.sol";
Expand Down Expand Up @@ -80,7 +81,8 @@ contract BaseFixture is Test {

ISafe safe;

// robosaver module
// robosaver module & factory
RoboSaverVirtualModuleFactory roboModuleFactory;
RoboSaverVirtualModule roboModule;

// Keeper address (forwarder): https://docs.chain.link/chainlink-automation/guides/forwarder#securing-your-upkeep
Expand Down Expand Up @@ -113,40 +115,27 @@ contract BaseFixture is Test {

bouncerContract = new Bouncer(address(safe), address(rolesModule), SET_ALLOWANCE_SELECTOR);

roboModule = new RoboSaverVirtualModule(address(delayModule), address(rolesModule), EURE_BUFFER, SLIPPAGE);
roboModuleFactory = new RoboSaverVirtualModuleFactory();
// fund the factory with LINK for task top up
deal(LINK, address(roboModuleFactory), LINK_FOR_TASK_TOP_UP);

// enable robo module in the delay & gnosis safe for tests flow
vm.startPrank(address(safe));

// create from factory new robo virtual module
roboModuleFactory.createVirtualModule(address(delayModule), address(delayModule), EURE_BUFFER, SLIPPAGE);
petrovska-petro marked this conversation as resolved.
Show resolved Hide resolved
(address roboModuleAddress, uint256 upkeepId) = roboModuleFactory.virtualModules(address(safe));

roboModule = RoboSaverVirtualModule(roboModuleAddress);
// deduct keeper from the registry and factory upkeep id rerieved from factory storage
keeper = CL_REGISTRY.getForwarder(upkeepId);

delayModule.enableModule(address(roboModule));
delayModule.enableModule(address(safe));

safe.enableModule(address(delayModule));
safe.enableModule(address(rolesModule));

// registering the task in CL automation service
deal(LINK, address(safe), LINK_FOR_TASK_TOP_UP);
IERC20(LINK).approve(address(CL_REGISTRAR), LINK_FOR_TASK_TOP_UP);

IKeeperRegistrar.RegistrationParams memory registrationParams = IKeeperRegistrar.RegistrationParams({
name: string.concat(roboModule.name(), "-", _addressToString(address(safe))),
encryptedEmail: "",
upkeepContract: address(roboModule),
gasLimit: 2_000_000,
adminAddress: address(safe),
triggerType: 0,
checkData: "",
triggerConfig: "",
offchainConfig: "",
amount: LINK_FOR_TASK_TOP_UP
});

uint256 upkeepID = CL_REGISTRAR.registerUpkeep(registrationParams);
assertNotEq(upkeepID, 0);

keeper = CL_REGISTRY.getForwarder(upkeepID);
roboModule.setKeeper(keeper);

vm.stopPrank();

vm.prank(SAFE_EOA_SIGNER);
Expand All @@ -171,15 +160,23 @@ contract BaseFixture is Test {

deal(BPT_STEUR_EURE, address(safe), EURE_TO_MINT);

_labelKeyContracts();
}

/// @dev Labels key contracts for tracing
function _labelKeyContracts() internal {
vm.label(EURE, "EURE");
vm.label(WETH, "WETH");
vm.label(LINK, "LINK");
vm.label(address(safe), "GNOSIS_SAFE");
vm.label(address(delayModule), "DELAY_MODULE");
vm.label(address(bouncerContract), "BOUNCER_CONTRACT");
vm.label(address(rolesModule), "ROLES_MODULE");
vm.label(address(roboModule), "ROBO_MODULE");
vm.label(BPT_STEUR_EURE, "BPT_STEUR_EURE");
vm.label(address(roboModule.BALANCER_VAULT()), "BALANCER_VAULT");
vm.label(address(CL_REGISTRY), "CL_REGISTRY");
vm.label(address(CL_REGISTRAR), "CL_REGISTRAR");
}

function _addressToString(address _addr) internal pure returns (string memory) {
Expand Down
Loading