Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
52534e9
chore: forge lint
antoncoding Feb 14, 2025
29f0b40
feat: deposit build
antoncoding Feb 14, 2025
7a29e7f
feat: init deposit contract
antoncoding Feb 16, 2025
50e59c5
test: intent deposit fork test
antoncoding Feb 16, 2025
98695da
fix: old derive fork tests
antoncoding Feb 16, 2025
90840bf
test: make sure rsETH in subaccount increases
antoncoding Feb 16, 2025
c06df0b
chore: lint
antoncoding Feb 16, 2025
25238ed
test: executor cannot deposit into wrong subaccounts
antoncoding Feb 16, 2025
177c063
chore: update test contract prefix to FORK_LYRA to be compatible with…
antoncoding Feb 16, 2025
9f13e17
chore: rename event
antoncoding Feb 16, 2025
0f88640
fix: check against Matching
antoncoding Feb 17, 2025
3e83d86
Merge pull request #1 from antoncoding/feat/withdrawal-intent
antoncoding Feb 17, 2025
db7e594
feat: stake intent
antoncoding Feb 17, 2025
18ce15b
fix: simplify StakeDRV intent + use SafeERC20
antoncoding Feb 17, 2025
2f8ce95
chore: add rescue function just in case
antoncoding Feb 17, 2025
4b42a68
docs: update comments
antoncoding Feb 17, 2025
f6f4664
test: add non-executor revert case
antoncoding Feb 17, 2025
00a755f
test: more revert cases
antoncoding Feb 17, 2025
3232459
feat: implement rate limiting
antoncoding Feb 20, 2025
47aa4ad
chore: rename constant
antoncoding Mar 3, 2025
3093bd5
test: add unit tests for buckets
antoncoding Mar 9, 2025
9964159
fix: [audit-m1] rugpull by exploited executor key with malicious Deri…
antoncoding Apr 9, 2025
0b4ba73
fix: [audit-low-1] Fail-open pattern on maxFee (#3)
antoncoding Apr 9, 2025
1cd53d6
fix: [informational] All informational findings (#4)
antoncoding Apr 9, 2025
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/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ jobs:
run: forge test --match-contract FORK_LYRA_ --fork-url https://rpc.lyra.finance

- name: Run Mainnet fork tests
run: forge test --match-contract FORK_MAINNET_ --fork-url https://mainnet.infura.io/v3/743507feddbd4a8088614092511076bc -vvv
run: forge test --match-contract FORK_MAINNET_ --fork-url https://mainnet.infura.io/v3/b3801473275f4a0a846ea7fe5a629349 -vvv
62 changes: 62 additions & 0 deletions src/intents/IntentExecutorBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {IERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "../../lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {SafeERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title IntentExecutorBase
* @notice A shared contract that allows authorized EOAs to execute intents
*/
abstract contract IntentExecutorBase is Ownable {
using SafeERC20 for IERC20;

/**
* @notice Whether the account is an intent executor
*/
mapping(address => bool) public isIntentExecutor;

/**
* @notice The error emitted when the caller is not an intent executor
*/
error NotIntentExecutor();

/**
* @notice The event emitted when an intent executor is set
*/
event IntentExecutorSet(address indexed executor, bool isIntentExecutor);

/**
* @notice Set an EOA as an intent executor
* @param _executor The EOA address
* @param _isIntentExecutor Whether the EOA is an intent executor
*/
function setIntentExecutor(address _executor, bool _isIntentExecutor) external onlyOwner {
isIntentExecutor[_executor] = _isIntentExecutor;

emit IntentExecutorSet(_executor, _isIntentExecutor);
}

/**
* @notice Allow owner to transfer any token out of the contract
* @param token The address of the token to rescue
*/
function rescueToken(address token) external onlyOwner {
// intentionally ignore return value
address(token).call(abi.encodeWithSignature("transfer(address,uint256)", msg.sender, IERC20(token).balanceOf(address(this))));
}

/**
* @notice Verify that the caller is an intent executor
*/
function _verifyIntentExecutor() internal view {
if (!isIntentExecutor[msg.sender]) revert NotIntentExecutor();
}

modifier onlyIntentExecutor() {
_verifyIntentExecutor();
_;
}
}
40 changes: 40 additions & 0 deletions src/intents/StakeDRVIntent.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {IERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IStakedDRV} from "../interfaces/derive/IStakedDRV.sol";

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

/**
* @title StakeDRVIntent
* @notice A shared contract that allows executor to help users auto stake DRV from smart wallet
* @dev Users who wish to have the auto-stake feature need to approve this contract to spend their DRV
*/
contract StakeDRVIntent is IntentExecutorBase {
address public immutable DRV;
address public immutable StakedDRV;

using SafeERC20 for IERC20;

event IntentStakeDRV(address indexed scw, uint256 amount);

constructor(address _drv, address _stakedDRV) {
DRV = _drv;
StakedDRV = _stakedDRV;
IERC20(_drv).approve(address(_stakedDRV), type(uint256).max);
}

/**
* @notice Execute a stake intent to auto stake DRV.
* @dev The SCW must have approved this contract to spend the token.
* @param scw The light account address
* @param amount The amount of DRV to stake
*/
function executeStakeDRVIntent(address scw, uint256 amount) external onlyIntentExecutor {
IERC20(DRV).safeTransferFrom(scw, address(this), amount);
IStakedDRV(StakedDRV).convertTo(amount, scw);
emit IntentStakeDRV(scw, amount);
}
}
126 changes: 126 additions & 0 deletions src/intents/SubaccountDepositIntent.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {IERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IntentExecutorBase} from "./IntentExecutorBase.sol";
import {ISubaccounts} from "../interfaces/ISubaccounts.sol";
import {IERC20BasedAsset} from "../interfaces/derive/IERC20BasedAsset.sol";
import {IMatching} from "../interfaces/derive/IMatching.sol";
import {IStandardManager} from "../interfaces/derive/IStandardManager.sol";
import {IPMRM2} from "../interfaces/derive/IPMRM2.sol";
import {SafeERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title SubaccountDepositIntent
* @notice A shared contract that allows authorized user to deposit LightAccount tokens into Derive Subaccounts
* @dev Users who wish to have the auto-deposit feature need to approve this contract to spend their tokens
*/
contract SubaccountDepositIntent is IntentExecutorBase {
using SafeERC20 for IERC20;

/// @dev The matching contract user deposits their subaccounts into to trade on Derive.
IMatching public immutable MATCHING;

/// @dev Derive Subaccounts
ISubaccounts public immutable SUBACCOUNTS;

/// @dev Special derive asset that's the base unit of the manager accounting system.
address public immutable CASH;

error SubaccountOwnerMismatch();
error DeriveAssetNotAllowed();

event IntentDeposit(uint256 indexed subaccountId, address indexed scw, address indexed token, uint256 amount);
event ManagerTypeSet(address indexed manager, uint256 indexed managerType);

// The type of manager that is allowed to be used
enum ManagerType {
None,
Standard,
PM2
}

// Derive v2 asset addresses that are allowed to be deposited for intent executors
mapping(address manager => ManagerType) public managerTypes;

constructor(IMatching _matching, address _cash) {
MATCHING = _matching;

SUBACCOUNTS = ISubaccounts(_matching.subAccounts());

CASH = _cash;
}

/**
* @notice Route tokens to a subaccount
* @param scw The light account address
* @param subaccountId The Derive subaccount ID
* @param deriveAsset The derive v2 asset address (IAsset)
* @param amount The amount of tokens to route
*/
function executeDepositIntent(address scw, uint256 subaccountId, address deriveAsset, uint256 amount)
external
onlyIntentExecutor
{
// Can only deposit to subaccounts that are owned by the SCW
_verifySubaccountOwner(subaccountId, scw);

// Can only deposit to derive v2 assets that are allowed
if (!_isAllowedDeriveAsset(subaccountId, deriveAsset)) revert DeriveAssetNotAllowed();

IERC20 token = IERC20BasedAsset(deriveAsset).wrappedAsset();
token.safeTransferFrom(scw, address(this), amount);
token.safeApprove(address(deriveAsset), amount);

IERC20BasedAsset(deriveAsset).deposit(subaccountId, amount);

emit IntentDeposit(subaccountId, scw, address(token), amount);
}

/**
* @notice Verify that the subaccount owner is correct
* @param subaccountId The Derive subaccount ID
* @param scw The LightAccount address
*/
function _verifySubaccountOwner(uint256 subaccountId, address scw) internal view {
if (MATCHING.subAccountToOwner(subaccountId) != scw) revert SubaccountOwnerMismatch();
}

/**
* @notice Set the allowed derive v2 asset
* @param manager The derive v2 manager
* @param managerType The type of manager
*/
function setManagerTypes(address manager, ManagerType managerType) external onlyOwner {
managerTypes[manager] = managerType;

emit ManagerTypeSet(manager, uint256(managerType));
}

/**
* @notice Check if the derive asset is valid given a subaccountId
* @dev We first check if the subaccountId is managed by a legitimate manager, then based on manager type,
* read the allowed derive asset list
* @param subaccountId subaccount ID
* @param deriveAsset address of the derive asset
*/
function _isAllowedDeriveAsset(uint256 subaccountId, address deriveAsset) internal view returns (bool) {
if (deriveAsset == CASH) return true;

address manager = SUBACCOUNTS.manager(subaccountId);

ManagerType managerType = managerTypes[manager];

if (managerType == ManagerType.None) {
return false;
} else if (managerType == ManagerType.Standard) {
IStandardManager.AssetDetail memory params = IStandardManager(manager).assetDetails(deriveAsset);
return params.isWhitelisted;
} else if (managerType == ManagerType.PM2) {
IPMRM2.CollateralParameters memory params = IPMRM2(manager).getCollateralParameters(deriveAsset);
return params.isEnabled;
} else {
return false;
}
}
}
Loading