Skip to content

Commit 1aff04e

Browse files
committed
feat: ERC7540 deposit actions
1 parent afc494c commit 1aff04e

File tree

4 files changed

+311
-0
lines changed

4 files changed

+311
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.26;
3+
4+
import {IERC4626} from "@openzeppelin-contracts/interfaces/IERC4626.sol";
5+
import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol";
6+
7+
import {InstructionLib} from "../libraries/Instruction.sol";
8+
9+
import {Interval} from "./schedules/Interval.sol";
10+
import {OtimFee} from "./fee-models/OtimFee.sol";
11+
12+
import {IAction} from "./interfaces/IAction.sol";
13+
import {
14+
IRequestDepositERC7540Action,
15+
INSTRUCTION_TYPEHASH,
16+
ARGUMENTS_TYPEHASH
17+
} from "./interfaces/IRequestDepositERC7540Action.sol";
18+
import {IERC7540Deposit} from "./external/IERC7540.sol";
19+
20+
import {InvalidArguments, InsufficientBalance, TotalSharesTooLow, MaxDepositTooLow} from "./errors/Errors.sol";
21+
22+
/// @title RequestDepositERC7540Action
23+
/// @author Otim Labs, Inc.
24+
/// @notice an Action that submits an asynchronous deposit request to an ERC7540 vault
25+
contract RequestDepositERC7540Action is IAction, IRequestDepositERC7540Action, Interval, OtimFee {
26+
constructor(address feeTokenRegistryAddress, address treasuryAddress, uint256 gasConstant_)
27+
OtimFee(feeTokenRegistryAddress, treasuryAddress, gasConstant_)
28+
{}
29+
30+
/// @inheritdoc IAction
31+
function argumentsHash(bytes calldata arguments) public pure returns (bytes32, bytes32) {
32+
return (INSTRUCTION_TYPEHASH, hash(abi.decode(arguments, (RequestDepositERC7540))));
33+
}
34+
35+
/// @inheritdoc IRequestDepositERC7540Action
36+
function hash(RequestDepositERC7540 memory arguments) public pure returns (bytes32) {
37+
return keccak256(
38+
abi.encode(
39+
ARGUMENTS_TYPEHASH,
40+
arguments.vault,
41+
arguments.assets,
42+
arguments.recipient,
43+
arguments.controller,
44+
arguments.minDeposit,
45+
arguments.minTotalShares,
46+
hash(arguments.schedule),
47+
hash(arguments.fee)
48+
)
49+
);
50+
}
51+
52+
/// @inheritdoc IAction
53+
function execute(
54+
InstructionLib.Instruction calldata instruction,
55+
InstructionLib.Signature calldata,
56+
InstructionLib.ExecutionState calldata executionState
57+
) external override returns (bool) {
58+
// initial gas measurement for fee calculation
59+
uint256 startGas = gasleft();
60+
61+
// decode the arguments from the instruction
62+
RequestDepositERC7540 memory arguments = abi.decode(instruction.arguments, (RequestDepositERC7540));
63+
64+
// if first execution, validate the input
65+
if (executionState.executionCount == 0) {
66+
if (
67+
arguments.vault == address(0) || arguments.assets == 0 || arguments.recipient == address(0)
68+
|| arguments.controller == address(0) || arguments.minTotalShares == 0
69+
) {
70+
revert InvalidArguments();
71+
}
72+
73+
checkStart(arguments.schedule);
74+
} else {
75+
checkInterval(arguments.schedule, executionState.lastExecuted);
76+
}
77+
78+
// get the underlying token (ERC7540 vaults are ERC4626-compatible for asset())
79+
address underlyingToken = IERC4626(arguments.vault).asset();
80+
81+
// check if the account has enough balance to request
82+
if (IERC20(underlyingToken).balanceOf(address(this)) < arguments.assets) {
83+
revert InsufficientBalance();
84+
}
85+
86+
// check if vault total shares is too low
87+
if (IERC4626(arguments.vault).totalSupply() < arguments.minTotalShares) {
88+
revert TotalSharesTooLow();
89+
}
90+
91+
// approve the vault to pull assets from the executing account (owner)
92+
// slither-disable-next-line unused-return
93+
IERC20(underlyingToken).approve(arguments.vault, arguments.assets);
94+
95+
// submit async deposit request: owner = address(this), controller from arguments
96+
uint256 requestId =
97+
IERC7540Deposit(arguments.vault).requestDeposit(arguments.assets, arguments.controller, address(this));
98+
99+
// if the request amount is less than the minimum deposit amount, revert
100+
if (
101+
IERC7540Deposit(arguments.vault).pendingDepositRequest(requestId, arguments.controller)
102+
< arguments.minDeposit
103+
) {
104+
revert MaxDepositTooLow();
105+
}
106+
107+
// charge the fee
108+
chargeFee(startGas - gasleft(), arguments.fee);
109+
110+
// this action has no auto-deactivation cases
111+
return false;
112+
}
113+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.26;
3+
4+
import {IERC4626} from "@openzeppelin-contracts/interfaces/IERC4626.sol";
5+
import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol";
6+
7+
import {InstructionLib} from "../libraries/Instruction.sol";
8+
9+
import {OtimFee} from "./fee-models/OtimFee.sol";
10+
11+
import {IAction} from "./interfaces/IAction.sol";
12+
import {
13+
ISweepRequestDepositERC7540Action,
14+
INSTRUCTION_TYPEHASH,
15+
ARGUMENTS_TYPEHASH
16+
} from "./interfaces/ISweepRequestDepositERC7540Action.sol";
17+
import {IERC7540Deposit} from "./external/IERC7540.sol";
18+
19+
import {InvalidArguments, BalanceUnderThreshold, TotalSharesTooLow, MaxDepositTooLow} from "./errors/Errors.sol";
20+
21+
/// @title SweepRequestDepositERC7540Action
22+
/// @author Otim Labs, Inc.
23+
/// @notice an Action that submits an async deposit request to an ERC7540 vault when the underlying balance is greater than or equal to a threshold
24+
contract SweepRequestDepositERC7540Action is IAction, ISweepRequestDepositERC7540Action, OtimFee {
25+
constructor(address feeTokenRegistryAddress, address treasuryAddress, uint256 gasConstant_)
26+
OtimFee(feeTokenRegistryAddress, treasuryAddress, gasConstant_)
27+
{}
28+
29+
/// @inheritdoc IAction
30+
function argumentsHash(bytes calldata arguments) public pure returns (bytes32, bytes32) {
31+
return (INSTRUCTION_TYPEHASH, hash(abi.decode(arguments, (SweepRequestDepositERC7540))));
32+
}
33+
34+
/// @inheritdoc ISweepRequestDepositERC7540Action
35+
function hash(SweepRequestDepositERC7540 memory arguments) public pure returns (bytes32) {
36+
return keccak256(
37+
abi.encode(
38+
ARGUMENTS_TYPEHASH,
39+
arguments.vault,
40+
arguments.recipient,
41+
arguments.controller,
42+
arguments.threshold,
43+
arguments.endBalance,
44+
arguments.minDeposit,
45+
arguments.minTotalShares,
46+
hash(arguments.fee)
47+
)
48+
);
49+
}
50+
51+
/// @inheritdoc IAction
52+
function execute(
53+
InstructionLib.Instruction calldata instruction,
54+
InstructionLib.Signature calldata,
55+
InstructionLib.ExecutionState calldata executionState
56+
) external override returns (bool) {
57+
// initial gas measurement for fee calculation
58+
uint256 startGas = gasleft();
59+
60+
// decode the arguments from the instruction
61+
SweepRequestDepositERC7540 memory arguments = abi.decode(instruction.arguments, (SweepRequestDepositERC7540));
62+
63+
// if first execution, validate the input
64+
if (executionState.executionCount == 0) {
65+
if (
66+
arguments.vault == address(0) || arguments.recipient == address(0) || arguments.controller == address(0)
67+
|| arguments.endBalance > arguments.threshold || arguments.minTotalShares == 0
68+
) {
69+
revert InvalidArguments();
70+
}
71+
}
72+
73+
// get the underlying token
74+
address underlyingToken = IERC4626(arguments.vault).asset();
75+
76+
// get the account's token balance
77+
uint256 balance = IERC20(underlyingToken).balanceOf(address(this));
78+
79+
// if the balance is under the threshold or equal to the endBalance, revert.
80+
// the endBalance check is to prevent the instruction from executing
81+
// when threshold == endBalance == balance because in this case we would have requestAmount == 0
82+
// slither-disable-next-line incorrect-equality
83+
if (balance < arguments.threshold || balance == arguments.endBalance) {
84+
revert BalanceUnderThreshold();
85+
}
86+
87+
// check if vault total shares is too low
88+
if (IERC4626(arguments.vault).totalSupply() < arguments.minTotalShares) {
89+
revert TotalSharesTooLow();
90+
}
91+
92+
// initialize request amount
93+
uint256 requestAmount = balance - arguments.endBalance;
94+
95+
// approve the vault to pull assets from the executing account (owner)
96+
// slither-disable-next-line unused-return
97+
IERC20(underlyingToken).approve(arguments.vault, requestAmount);
98+
99+
// submit async deposit request: owner = address(this), controller from arguments
100+
uint256 requestId =
101+
IERC7540Deposit(arguments.vault).requestDeposit(requestAmount, arguments.controller, address(this));
102+
103+
// if the request amount is less than the minimum deposit amount, revert
104+
if (
105+
IERC7540Deposit(arguments.vault).pendingDepositRequest(requestId, arguments.controller)
106+
< arguments.minDeposit
107+
) {
108+
revert MaxDepositTooLow();
109+
}
110+
111+
// charge the fee
112+
chargeFee(startGas - gasleft(), arguments.fee);
113+
114+
// this action has no auto-deactivation cases
115+
return false;
116+
}
117+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.26;
3+
4+
import {IInterval} from "../schedules/interfaces/IInterval.sol";
5+
import {IOtimFee} from "../fee-models/interfaces/IOtimFee.sol";
6+
7+
bytes32 constant INSTRUCTION_TYPEHASH = keccak256(
8+
"Instruction(uint256 salt,uint256 maxExecutions,address action,RequestDepositERC7540 requestDepositERC7540)RequestDepositERC7540(address vault,uint256 assets,address recipient,address controller,uint256 minTotalShares,Schedule schedule,Fee fee)Fee(address token,uint256 maxBaseFeePerGas,uint256 maxPriorityFeePerGas,uint256 executionFee)Schedule(uint256 startAt,uint256 startBy,uint256 interval,uint256 timeout)"
9+
);
10+
11+
bytes32 constant ARGUMENTS_TYPEHASH = keccak256(
12+
"RequestDepositERC7540(address vault,uint256 assets,address recipient,address controller,uint256 minTotalShares,Schedule schedule,Fee fee)Fee(address token,uint256 maxBaseFeePerGas,uint256 maxPriorityFeePerGas,uint256 executionFee)Schedule(uint256 startAt,uint256 startBy,uint256 interval,uint256 timeout)"
13+
);
14+
15+
/// @title IRequestDepositERC7540Action
16+
/// @author Otim Labs, Inc.
17+
/// @notice interface for RequestDepositERC7540Action contract
18+
interface IRequestDepositERC7540Action is IInterval, IOtimFee {
19+
/// @notice arguments for the RequestDepositERC7540Action contract
20+
/// @param vault - the address of the ERC7540 vault to request deposit to
21+
/// @param assets - the amount of assets to request for deposit
22+
/// @param recipient - the intended receiver of shares when the deposit is claimed
23+
/// @param controller - the ERC7540 controller who can later claim the deposit
24+
/// @param minDeposit - the minimum deposit amount to trigger the request
25+
/// @param minTotalShares - the minimum total shares of the vault before the request
26+
/// @param schedule - the schedule parameters for the request
27+
/// @param fee - the fee Otim will charge for the request
28+
struct RequestDepositERC7540 {
29+
address vault;
30+
uint256 assets;
31+
address recipient;
32+
address controller;
33+
uint256 minDeposit;
34+
uint256 minTotalShares;
35+
Schedule schedule;
36+
Fee fee;
37+
}
38+
39+
/// @notice calculates the EIP-712 hash of the RequestDepositERC7540 struct
40+
function hash(RequestDepositERC7540 memory arguments) external pure returns (bytes32);
41+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.26;
3+
4+
import {IOtimFee} from "../fee-models/interfaces/IOtimFee.sol";
5+
6+
bytes32 constant INSTRUCTION_TYPEHASH = keccak256(
7+
"Instruction(uint256 salt,uint256 maxExecutions,address action,SweepRequestDepositERC7540 sweepRequestDepositERC7540)Fee(address token,uint256 maxBaseFeePerGas,uint256 maxPriorityFeePerGas,uint256 executionFee)SweepRequestDepositERC7540(address vault,address recipient,address controller,uint256 threshold,uint256 endBalance,Fee fee)"
8+
);
9+
10+
bytes32 constant ARGUMENTS_TYPEHASH = keccak256(
11+
"SweepRequestDepositERC7540(address vault,address recipient,address controller,uint256 threshold,uint256 endBalance,Fee fee)Fee(address token,uint256 maxBaseFeePerGas,uint256 maxPriorityFeePerGas,uint256 executionFee)"
12+
);
13+
14+
/// @title ISweepRequestDepositERC7540Action
15+
/// @author Otim Labs, Inc.
16+
/// @notice interface for SweepRequestDepositERC7540Action contract
17+
interface ISweepRequestDepositERC7540Action is IOtimFee {
18+
/// @notice arguments for the SweepRequestDepositERC7540Action contract
19+
/// @param vault - the address of the ERC7540 vault to request deposit to
20+
/// @param recipient - the intended receiver of shares when the deposit is later claimed
21+
/// @param controller - the ERC7540 controller for the request
22+
/// @param threshold - the account's underlying balance threshold to trigger the sweep
23+
/// @param endBalance - the account's balance after the sweep
24+
/// @param minDeposit - the minimum deposit amount to trigger the sweep
25+
/// @param minTotalShares - the minimum total shares of the vault before the request
26+
/// @param fee - the fee Otim will charge for the request
27+
struct SweepRequestDepositERC7540 {
28+
address vault;
29+
address recipient;
30+
address controller;
31+
uint256 threshold;
32+
uint256 endBalance;
33+
uint256 minDeposit;
34+
uint256 minTotalShares;
35+
Fee fee;
36+
}
37+
38+
/// @notice calculates the EIP-712 hash of the SweepRequestDepositERC7540 struct
39+
function hash(SweepRequestDepositERC7540 memory arguments) external pure returns (bytes32);
40+
}

0 commit comments

Comments
 (0)