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 all 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
3 changes: 3 additions & 0 deletions .github/codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ coverage:
status:
project: off
patch: off
ignore: # exclude these folders from the report
- "script"
- "test"
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
107 changes: 51 additions & 56 deletions src/RoboSaverVirtualModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,14 @@ import "@balancer-v2/interfaces/contracts/pool-stable/StablePoolUserData.sol";

import {KeeperCompatibleInterface} from "@chainlink/automation/interfaces/KeeperCompatibleInterface.sol";

import {VirtualModule} from "./types/DataTypes.sol";

/// @title RoboSaver: turn your Gnosis Pay card into an automated savings account!
/// @author onchainification.xyz
/// @notice Deposit and withdraw $EURe from your Gnosis Pay card to a liquidity pool
contract RoboSaverVirtualModule is
KeeperCompatibleInterface // 1 inherited component
{
/*//////////////////////////////////////////////////////////////////////////
DATA TYPES
//////////////////////////////////////////////////////////////////////////*/

/// @notice Enum representing the different types of pool actions
/// @custom:value0 WITHDRAW Withdraw $EURe from the pool to the card
/// @custom:value1 DEPOSIT Deposit $EURe from the card into the pool
/// @custom:value2 CLOSE Close the pool position by withdrawing all to $EURe
/// @custom:value3 EXEC_QUEUE_POOL_ACTION Execute the queued pool action
enum PoolAction {
WITHDRAW,
DEPOSIT,
CLOSE,
EXEC_QUEUE_POOL_ACTION
}

/// @notice Struct representing the data needed to execute a queued transaction
/// @dev Nonce allows us to determine if the transaction queued originated from this virtual module
/// @param nonce The nonce of the queued transaction
/// @param target The address of the target contract
/// @param payload The payload of the transaction to be executed on the target contract
struct QueuedTx {
uint256 nonce;
address target;
bytes payload;
}

/*//////////////////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -68,10 +43,11 @@ contract RoboSaverVirtualModule is

IComposableStablePool immutable BPT_STEUR_EURE;

address public immutable FACTORY;

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

IDelayModifier public delayModule;
IRolesModifier public rolesModule;

Expand All @@ -80,7 +56,7 @@ contract RoboSaverVirtualModule is
uint16 public slippage;

/// @dev Keeps track of the transaction queued up by the virtual module and allows internally to call `executeNextTx`
QueuedTx public queuedTx;
VirtualModule.QueuedTx public queuedTx;

/*//////////////////////////////////////////////////////////////////////////
PRIVATE STORAGE
Expand Down Expand Up @@ -149,6 +125,7 @@ contract RoboSaverVirtualModule is

error NotKeeper(address agent);
error NotAdmin(address agent);
error NeitherAdminNorFactory(address agent);

error ZeroAddressValue();
error ZeroUintValue();
Expand All @@ -174,15 +151,24 @@ contract RoboSaverVirtualModule is
_;
}

/// @notice Enforce that the function is called by the admin or the factory only
modifier onlyAdminOrFactory() {
if (msg.sender != CARD && msg.sender != FACTORY) revert NeitherAdminNorFactory(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;
slippage = _slippage;

_setBuffer(_buffer);
_setSlippage(_slippage);

CARD = delayModule.avatar();

Expand Down Expand Up @@ -223,7 +209,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 onlyAdminOrFactory {
if (_keeper == address(0)) revert ZeroAddressValue();

address oldKeeper = keeper;
Expand All @@ -235,23 +221,13 @@ contract RoboSaverVirtualModule is
/// @notice Assigns a new value for the buffer responsible for deciding when there is a surplus
/// @param _buffer The value of the new buffer
function setBuffer(uint256 _buffer) external onlyAdmin {
if (_buffer == 0) revert ZeroUintValue();

uint256 oldBuffer = buffer;
buffer = _buffer;

emit SetBuffer(msg.sender, oldBuffer, buffer);
_setBuffer(_buffer);
}

/// @notice Adjust the maximum slippage the user is comfortable with
/// @param _slippage The value of the new slippage in bps (so 10_000 is 100%)
function setSlippage(uint16 _slippage) external onlyAdmin {
if (_slippage >= MAX_BPS) revert TooHighBps();

uint16 oldSlippage = slippage;
slippage = _slippage;

emit SetSlippage(msg.sender, oldSlippage, slippage);
_setSlippage(_slippage);
}

/// @notice Check if there is a surplus or deficit of $EURe on the card
Expand All @@ -275,7 +251,7 @@ contract RoboSaverVirtualModule is
if (queuedTx.nonce != 0) {
/// @notice check if the transaction is still in cooldown or ready to exec
if (_isInCoolDown(queuedTx.nonce)) return (false, bytes("Internal transaction in cooldown status"));
return (true, abi.encode(PoolAction.EXEC_QUEUE_POOL_ACTION, 0));
return (true, abi.encode(VirtualModule.PoolAction.EXEC_QUEUE_POOL_ACTION, 0));
}

uint256 balance = EURE.balanceOf(CARD);
Expand All @@ -290,14 +266,14 @@ contract RoboSaverVirtualModule is
uint256 deficit = dailyAllowance - balance + buffer;
uint256 withdrawableEure = bptBalance * BPT_STEUR_EURE.getRate() * (MAX_BPS - slippage) / 1e18 / MAX_BPS;
if (withdrawableEure < deficit) {
return (true, abi.encode(PoolAction.CLOSE, withdrawableEure));
return (true, abi.encode(VirtualModule.PoolAction.CLOSE, withdrawableEure));
} else {
return (true, abi.encode(PoolAction.WITHDRAW, deficit));
return (true, abi.encode(VirtualModule.PoolAction.WITHDRAW, deficit));
}
} else if (balance > dailyAllowance + buffer) {
/// @notice there is a surplus; we need to deposit into the pool
uint256 surplus = balance - (dailyAllowance + buffer);
return (true, abi.encode(PoolAction.DEPOSIT, surplus));
return (true, abi.encode(VirtualModule.PoolAction.DEPOSIT, surplus));
}

/// @notice neither deficit nor surplus; no action needed
Expand All @@ -306,7 +282,8 @@ contract RoboSaverVirtualModule is

function performUpkeep(bytes calldata _performData) external override onlyKeeper {
// decode `_performData`
(PoolAction action, uint256 amount) = abi.decode(_performData, (PoolAction, uint256));
(VirtualModule.PoolAction action, uint256 amount) =
abi.decode(_performData, (VirtualModule.PoolAction, uint256));
_adjustPool(action, amount);
}

Expand All @@ -317,18 +294,18 @@ contract RoboSaverVirtualModule is
/// @notice Adjust the pool by depositing or withdrawing $EURe
/// @param _action The action to take: deposit or withdraw
/// @param _amount The amount of $EURe to deposit or withdraw
function _adjustPool(PoolAction _action, uint256 _amount) internal {
function _adjustPool(VirtualModule.PoolAction _action, uint256 _amount) internal {
if (!delayModule.isModuleEnabled(address(this))) revert VirtualModuleNotEnabled();
if (_isCleanQueueRequired()) delayModule.skipExpired();
if (_isExternalTxQueued()) revert ExternalTxIsQueued();

if (_action == PoolAction.WITHDRAW) {
if (_action == VirtualModule.PoolAction.WITHDRAW) {
_poolWithdrawal(_amount);
} else if (_action == PoolAction.DEPOSIT) {
} else if (_action == VirtualModule.PoolAction.DEPOSIT) {
_poolDeposit(_amount);
} else if (_action == PoolAction.CLOSE) {
} else if (_action == VirtualModule.PoolAction.CLOSE) {
_poolClose(_amount);
} else if (_action == PoolAction.EXEC_QUEUE_POOL_ACTION) {
} else if (_action == VirtualModule.PoolAction.EXEC_QUEUE_POOL_ACTION) {
_executeQueuedTx();
}
}
Expand Down Expand Up @@ -449,7 +426,7 @@ contract RoboSaverVirtualModule is
: IDelayModifier.DelayModuleOperation.Call;
delayModule.execTransactionFromModule(_target, 0, _payload, operation);
uint256 cachedQueueNonce = delayModule.queueNonce();
queuedTx = QueuedTx(cachedQueueNonce, _target, _payload);
queuedTx = VirtualModule.QueuedTx(cachedQueueNonce, _target, _payload);

emit AdjustPoolTxDataQueued(_target, _payload, cachedQueueNonce);
}
Expand All @@ -473,4 +450,22 @@ contract RoboSaverVirtualModule is
anyExpiredTxs_ = true;
}
}

function _setSlippage(uint16 _slippage) internal {
if (_slippage >= MAX_BPS) revert TooHighBps();

uint16 oldSlippage = slippage;
slippage = _slippage;

emit SetSlippage(msg.sender, oldSlippage, slippage);
}

function _setBuffer(uint256 _buffer) internal {
if (_buffer == 0) revert ZeroUintValue();

uint256 oldBuffer = buffer;
buffer = _buffer;

emit SetBuffer(msg.sender, oldBuffer, buffer);
}
}
Loading