-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Intent Contracts #4
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
Merged
Merged
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
52534e9
chore: forge lint
antoncoding 29f0b40
feat: deposit build
antoncoding 7a29e7f
feat: init deposit contract
antoncoding 50e59c5
test: intent deposit fork test
antoncoding 98695da
fix: old derive fork tests
antoncoding 90840bf
test: make sure rsETH in subaccount increases
antoncoding c06df0b
chore: lint
antoncoding 25238ed
test: executor cannot deposit into wrong subaccounts
antoncoding 177c063
chore: update test contract prefix to FORK_LYRA to be compatible with…
antoncoding 9f13e17
chore: rename event
antoncoding 0f88640
fix: check against Matching
antoncoding 3e83d86
Merge pull request #1 from antoncoding/feat/withdrawal-intent
antoncoding db7e594
feat: stake intent
antoncoding 18ce15b
fix: simplify StakeDRV intent + use SafeERC20
antoncoding 2f8ce95
chore: add rescue function just in case
antoncoding 4b42a68
docs: update comments
antoncoding f6f4664
test: add non-executor revert case
antoncoding 00a755f
test: more revert cases
antoncoding 3232459
feat: implement rate limiting
antoncoding 47aa4ad
chore: rename constant
antoncoding 3093bd5
test: add unit tests for buckets
antoncoding 9964159
fix: [audit-m1] rugpull by exploited executor key with malicious Deri…
antoncoding 0b4ba73
fix: [audit-low-1] Fail-open pattern on maxFee (#3)
antoncoding 1cd53d6
fix: [informational] All informational findings (#4)
antoncoding File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| _; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.