Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
61 changes: 61 additions & 0 deletions src/intents/IntentExecutorBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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
*/
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 {
IERC20(token).safeTransfer(owner(), 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();
_;
}
}
37 changes: 37 additions & 0 deletions src/intents/StakeDRVIntent.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {IERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.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;

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).transferFrom(scw, address(this), amount);
IStakedDRV(StakedDRV).convertTo(amount, scw);
emit IntentStakeDRV(scw, amount);
}
}
60 changes: 60 additions & 0 deletions src/intents/SubaccountDepositIntent.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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 {IntentExecutorBase} from "./IntentExecutorBase.sol";
import {IERC20BasedAsset} from "../interfaces/derive/IERC20BasedAsset.sol";
import {IMatching} from "../interfaces/derive/IMatching.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;

IMatching public immutable MATCHING;

error SubaccountOwnerMismatch();

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

constructor(IMatching _matching) {
MATCHING = _matching;
}

/**
* @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);

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();
}
}
124 changes: 124 additions & 0 deletions src/intents/WithdrawBridgeIntent.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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 {ILightAccount} from "../interfaces/ILightAccount.sol";
import {ISocketWithdrawWrapper} from "../interfaces/derive/ISocketWithdrawWrapper.sol";
import {IOFTWithdrawWrapper} from "../interfaces/derive/IOFTWithdrawWrapper.sol";
import {SafeERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title WithdrawBridgeIntent
* @notice A shared contract that allows executor to help users to withdraw tokens from LightAccount off Derive through bridges
* @dev Users who wish to have the auto-withdraw feature need to approve this contract to spend their tokens
*
* @dev Trust Assumptions:
* - Users must trust the executor not to arbitrarily execute withdrawals.
* - Users must trust the owner to not add malicious executor
* - Users rely on executors to provide a valid maxFee for each action to avoid being charged high fees by bridges.
*/
contract WithdrawBridgeIntent is IntentExecutorBase {
using SafeERC20 for IERC20;

ISocketWithdrawWrapper public immutable SOCKET_BRIDGE;

IOFTWithdrawWrapper public immutable IOFT_BRIDGE;

error InvalidRecipient();
error FeeTooHigh();

event IntentWithdrawSocket(
address indexed scw,
address indexed token,
uint256 amount,
address recipient,
address controller,
address connector
);

event IntentWithdrawLZ(
address indexed scw, address indexed token, uint256 amount, address recipient, uint32 destEID
);

constructor(ISocketWithdrawWrapper _socketBridge, IOFTWithdrawWrapper _iOFTBridge) {
SOCKET_BRIDGE = _socketBridge;
IOFT_BRIDGE = _iOFTBridge;
}

/**
* @notice Execute a withdraw intent to auto bridge tokens off Derive.
* @dev The SCW must have approved this contract to spend the token, and set max fee for each token.
* @param scw The light account address
* @param token The ERC20 token address
* @param amount The amount of tokens to withdraw
* @param recipient The recipient address, must specify explictly as the SCW owner
* @param controller The Socket Controller address
* @param connector The Socket Connector address
*/
function executeWithdrawIntentSocket(
address scw,
address token,
uint256 amount,
uint256 maxFee,
address recipient,
address controller,
address connector,
uint256 gasLimit
) external onlyIntentExecutor {
IERC20(token).safeTransferFrom(scw, address(this), amount);
IERC20(token).safeApprove(address(SOCKET_BRIDGE), amount);

// The auto execution can only be triggered if the fee is less than the max fee set by the user
if (maxFee > 0) {
uint256 feeInToken = SOCKET_BRIDGE.getFeeInToken(token, controller, connector, gasLimit);
if (feeInToken > maxFee) revert FeeTooHigh();
}

// The recipient must be the owner of the SCW
if (ILightAccount(scw).owner() != recipient) {
revert InvalidRecipient();
}

SOCKET_BRIDGE.withdrawToChain(token, amount, recipient, controller, connector, gasLimit);

emit IntentWithdrawSocket(scw, token, amount, recipient, controller, connector);
}

/**
* @notice Execute a withdraw intent to auto bridge tokens off Derive through LayerZero OFT Wrapper.
* @dev The SCW must have approved this contract to spend the token, and set max fee for each token.
* @param scw The light account address
* @param token The ERC20 token address
* @param amount The amount of tokens to withdraw
* @param maxFee The maximum fee for the withdraw bridge
* @param recipient The recipient address, must be a valid recipient or the owner of the SCW
* @param destEID The destination EID
*/
function executeWithdrawIntentLZ(
address scw,
address token,
uint256 amount,
uint256 maxFee,
address recipient,
uint32 destEID
) external onlyIntentExecutor {
IERC20(token).safeTransferFrom(scw, address(this), amount);
IERC20(token).safeApprove(address(IOFT_BRIDGE), amount);

// The auto execution can only be triggered if the fee is less than the max fee set by the user
if (maxFee > 0) {
uint256 feeInToken = IOFT_BRIDGE.getFeeInToken(token, amount, destEID);
if (feeInToken > maxFee) revert FeeTooHigh();
}

// The recipient must be the owner of the SCW
if (ILightAccount(scw).owner() != recipient) {
revert InvalidRecipient();
}

IOFT_BRIDGE.withdrawToChain(token, amount, recipient, destEID);

emit IntentWithdrawLZ(scw, token, amount, recipient, destEID);
}
}
6 changes: 6 additions & 0 deletions src/interfaces/ILightAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

interface ILightAccount {
function owner() external view returns (address);
}
10 changes: 10 additions & 0 deletions src/interfaces/derive/IERC20BasedAsset.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.18;

import "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";

interface IERC20BasedAsset {
function wrappedAsset() external view returns (IERC20Metadata);
function deposit(uint256 recipientAccount, uint256 assetAmount) external;
function withdraw(uint256 accountId, uint256 assetAmount, address recipient) external;
}
6 changes: 6 additions & 0 deletions src/interfaces/derive/IMatching.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.18;

interface IMatching {
function subAccountToOwner(uint256 subAccountId) external view returns (address);
}
10 changes: 10 additions & 0 deletions src/interfaces/derive/IOFTWithdrawWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.18;

import "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";

interface IOFTWithdrawWrapper {
function withdrawToChain(address token, uint256 amount, address toAddress, uint32 destEID) external;

function getFeeInToken(address token, uint256 amount, uint32 destEID) external view returns (uint256);
}
20 changes: 20 additions & 0 deletions src/interfaces/derive/ISocketWithdrawWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.18;

import "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";

interface ISocketWithdrawWrapper {
function withdrawToChain(
address token,
uint256 amount,
address recipient,
address socketController,
address connector,
uint256 gasLimit
) external;

function getFeeInToken(address token, address controller, address connector, uint256 gasLimit)
external
view
returns (uint256);
}
8 changes: 8 additions & 0 deletions src/interfaces/derive/IStakedDRV.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.18;

import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";

interface IStakedDRV is IERC20 {
function convertTo(uint256 amount, address token) external;
}
6 changes: 3 additions & 3 deletions src/withdraw/LyraWithdrawWrapperV2New.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ contract LyraWithdrawWrapperV2New is Ownable {
mapping(address token => uint256) public staticPrice;
mapping(address => ControllerType) public controllerType;
address public feeRecipient;
uint public payloadSize = 161;
uint256 public payloadSize = 161;

constructor() payable {}

Expand All @@ -58,7 +58,7 @@ contract LyraWithdrawWrapperV2New is Ownable {
feeRecipient = newRecipient;
}

function setPayloadSize(uint newSize) external onlyOwner {
function setPayloadSize(uint256 newSize) external onlyOwner {
payloadSize = newSize;
}

Expand All @@ -79,7 +79,7 @@ contract LyraWithdrawWrapperV2New is Ownable {

function setStaticRates(address[] memory tokens, uint256[] memory rates) external onlyOwner {
require(tokens.length == rates.length, "Array length mismatch");
for (uint i = 0; i < tokens.length; i++) {
for (uint256 i = 0; i < tokens.length; i++) {
staticPrice[tokens[i]] = rates[i];
}
}
Expand Down
Loading