From b3cb8a8719e2dafdbe8a2a1c6d829f639006f36e Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 9 Nov 2023 01:46:05 +0530 Subject: [PATCH 01/20] interfaces and basic implementation --- .../prebuilts/unaudited/checkout/Checkout.sol | 76 +++++++++++++ .../prebuilts/unaudited/checkout/Executor.sol | 57 ++++++++++ .../unaudited/checkout/ICheckout.sol | 8 ++ .../unaudited/checkout/IExecutor.sol | 30 +++++ .../prebuilts/unaudited/checkout/IVault.sol | 19 ++++ .../prebuilts/unaudited/checkout/Vault.sol | 103 ++++++++++++++++++ 6 files changed, 293 insertions(+) create mode 100644 contracts/prebuilts/unaudited/checkout/Checkout.sol create mode 100644 contracts/prebuilts/unaudited/checkout/Executor.sol create mode 100644 contracts/prebuilts/unaudited/checkout/ICheckout.sol create mode 100644 contracts/prebuilts/unaudited/checkout/IExecutor.sol create mode 100644 contracts/prebuilts/unaudited/checkout/IVault.sol create mode 100644 contracts/prebuilts/unaudited/checkout/Vault.sol diff --git a/contracts/prebuilts/unaudited/checkout/Checkout.sol b/contracts/prebuilts/unaudited/checkout/Checkout.sol new file mode 100644 index 000000000..5a4443191 --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/Checkout.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@openzeppelin/contracts/proxy/Clones.sol"; + +import "./ICheckout.sol"; +import "./Vault.sol"; +import "./Executor.sol"; + +import "../../../external-deps/openzeppelin/utils/Create2.sol"; + +import "../../../extension/PermissionsEnumerable.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract Checkout is PermissionsEnumerable, ICheckout { + /// @dev Registry of vaults created through this Checkout + mapping(address => bool) isVaultRegistered; + + /// @dev Registry of executors created through this Checkout + mapping(address => bool) isExecutorRegistered; + + address public immutable vaultImplementation; + address public immutable executorImplementation; + + constructor( + address _defaultAdmin, + address _vaultImplementation, + address _executorImplementation + ) { + vaultImplementation = _vaultImplementation; + executorImplementation = _executorImplementation; + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + function createVault(address _vaultAdmin, bytes32 _salt) external payable returns (address) { + bytes32 salthash = keccak256(abi.encodePacked(msg.sender, _salt)); + address vault = Clones.cloneDeterministic(vaultImplementation, salthash); + + (bool success, ) = vault.call(abi.encodeWithSelector(Vault.initialize.selector, _vaultAdmin)); + + require(success, "Deployment failed"); + + isVaultRegistered[vault] = true; + + return vault; + } + + function createExecutor(address _executorAdmin, bytes32 _salt) external payable returns (address) { + bytes32 salthash = keccak256(abi.encodePacked(msg.sender, _salt)); + address executor = Clones.cloneDeterministic(executorImplementation, salthash); + + (bool success, ) = executor.call(abi.encodeWithSelector(Executor.initialize.selector, _executorAdmin)); + + require(success, "Deployment failed"); + + isExecutorRegistered[executor] = true; + + return executor; + } + + function authorizeVaultToExecutor(address _vault, address _executor) external { + require(IVault(_vault).canAuthorizeVaultToExecutor(msg.sender), "Not authorized"); + require(isExecutorRegistered[_executor], "Executor not found"); + + IVault(_vault).setExecutor(_executor); + } +} diff --git a/contracts/prebuilts/unaudited/checkout/Executor.sol b/contracts/prebuilts/unaudited/checkout/Executor.sol new file mode 100644 index 000000000..c39d9ae93 --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/Executor.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "./IExecutor.sol"; +import "./IVault.sol"; + +import "../../../lib/CurrencyTransferLib.sol"; +import "../../../eip/interface/IERC20.sol"; + +import "../../../extension/PermissionsEnumerable.sol"; +import "../../../extension/Initializable.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract Executor is Initializable, PermissionsEnumerable, IExecutor { + constructor() { + _disableInitializers(); + } + + function initialize(address _defaultAdmin) external initializer { + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + receive() external payable {} + + function execute(UserOp calldata op) external { + require(_canExecute(), "Not authorized"); + + if (op.valueToSend != 0) { + IVault(op.vault).transferTokensToExecutor(op.currency, op.valueToSend); + } + + bool success; + if (op.currency == CurrencyTransferLib.NATIVE_TOKEN) { + (success, ) = op.target.call{ value: op.valueToSend }(op.data); + } else { + if (op.approvalRequired) { + IERC20(op.currency).approve(op.target, op.valueToSend); + } + + (success, ) = op.target.call(op.data); + } + + require(success, "Execution failed"); + } + + function _canExecute() internal view returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + } +} diff --git a/contracts/prebuilts/unaudited/checkout/ICheckout.sol b/contracts/prebuilts/unaudited/checkout/ICheckout.sol new file mode 100644 index 000000000..582a29137 --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/ICheckout.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface ICheckout { + function createVault(address _vaultAdmin, bytes32 _salt) external payable returns (address); + + function createExecutor(address _executorAdmin, bytes32 _salt) external payable returns (address); +} diff --git a/contracts/prebuilts/unaudited/checkout/IExecutor.sol b/contracts/prebuilts/unaudited/checkout/IExecutor.sol new file mode 100644 index 000000000..1e41592dd --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/IExecutor.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface IExecutor { + /** + * @notice Details of the transaction to execute on target contract. + * + * @param target Address to send the transaction to + * + * @param currency Represents both native token and erc20 token + * + * @param vault Vault providing liquidity for this transaction + * + * @param approvalRequired If need to approve erc20 to the target contract + * + * @param valueToSend Transaction value to send - both native and erc20 + * + * @param data Transaction calldata + */ + struct UserOp { + address target; + address currency; + address vault; + bool approvalRequired; + uint256 valueToSend; + bytes data; + } + + function execute(UserOp calldata op) external; +} diff --git a/contracts/prebuilts/unaudited/checkout/IVault.sol b/contracts/prebuilts/unaudited/checkout/IVault.sol new file mode 100644 index 000000000..d924cdafa --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/IVault.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface IVault { + /// @dev Emitted when contract admin withdraws tokens. + event TokensWithdrawn(address _token, uint256 _amount); + + /// @dev Emitted when contract admin deposits tokens. + event TokensDeposited(address _token, uint256 _amount); + + /// @dev Emitted when executor contract withdraws tokens. + event TokensTransferredToExecutor(address indexed _executor, address _token, uint256 _amount); + + function transferTokensToExecutor(address _token, uint256 _amount) external; + + function setExecutor(address _executor) external; + + function canAuthorizeVaultToExecutor(address _expectedAdmin) external view returns (bool); +} diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol new file mode 100644 index 000000000..e6b20594e --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "./IVault.sol"; + +import "../../../lib/CurrencyTransferLib.sol"; +import "../../../eip/interface/IERC20.sol"; + +import "../../../extension/PermissionsEnumerable.sol"; +import "../../../extension/Initializable.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract Vault is Initializable, PermissionsEnumerable, IVault { + /// @dev Mapping from token address to total balance in the vault. + mapping(address => uint256) public tokenBalance; + + /// @dev Address of the executor for this vault. + address public executor; + + /// @dev Address of the Checkout entrypoint. + address public checkout; + + constructor() { + _disableInitializers(); + } + + function initialize(address _defaultAdmin) external initializer { + checkout = msg.sender; + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + function deposit(address _token, uint256 _amount) external payable { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized"); + + uint256 _actualAmount; + + if (_token == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == _amount, "!Amount"); + _actualAmount = _amount; + + tokenBalance[_token] += _actualAmount; + } else { + uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); + CurrencyTransferLib.safeTransferERC20(_token, msg.sender, address(this), _amount); + _actualAmount = IERC20(_token).balanceOf(address(this)) - balanceBefore; + + tokenBalance[_token] += _actualAmount; + } + + emit TokensDeposited(_token, _actualAmount); + } + + function withdraw(address _token, uint256 _amount) external { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized"); + + uint256 balance = tokenBalance[_token]; + // to prevent locking of direct-transferred tokens + tokenBalance[_token] = _amount > balance ? 0 : balance - _amount; + + CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); + + emit TokensWithdrawn(_token, _amount); + } + + function transferTokensToExecutor(address _token, uint256 _amount) external { + require(_canTransferTokens(), "Not authorized"); + + uint256 balance = tokenBalance[_token]; + require(balance >= _amount, "Not enough balance"); + + tokenBalance[_token] -= _amount; + + CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); + + emit TokensTransferredToExecutor(msg.sender, _token, _amount); + } + + function setExecutor(address _executor) external { + require(_canSetExecutor(), "Not authorized"); + + executor = _executor; + } + + function canAuthorizeVaultToExecutor(address _expectedAdmin) external view returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _expectedAdmin); + } + + function _canSetExecutor() internal view returns (bool) { + return msg.sender == checkout; + } + + function _canTransferTokens() internal view returns (bool) { + return msg.sender == executor; + } +} From 0efc4ef88a7e95a54d3028256f010b1dcacfffde Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 9 Nov 2023 01:57:05 +0530 Subject: [PATCH 02/20] update --- contracts/prebuilts/unaudited/checkout/Checkout.sol | 6 ++++++ contracts/prebuilts/unaudited/checkout/ICheckout.sol | 11 +++++++++++ contracts/prebuilts/unaudited/checkout/IVault.sol | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/contracts/prebuilts/unaudited/checkout/Checkout.sol b/contracts/prebuilts/unaudited/checkout/Checkout.sol index 5a4443191..dd69caa45 100644 --- a/contracts/prebuilts/unaudited/checkout/Checkout.sol +++ b/contracts/prebuilts/unaudited/checkout/Checkout.sol @@ -51,6 +51,8 @@ contract Checkout is PermissionsEnumerable, ICheckout { isVaultRegistered[vault] = true; + emit VaultCreated(vault, _vaultAdmin); + return vault; } @@ -64,6 +66,8 @@ contract Checkout is PermissionsEnumerable, ICheckout { isExecutorRegistered[executor] = true; + emit ExecutorCreated(executor, _executorAdmin); + return executor; } @@ -72,5 +76,7 @@ contract Checkout is PermissionsEnumerable, ICheckout { require(isExecutorRegistered[_executor], "Executor not found"); IVault(_vault).setExecutor(_executor); + + emit VaultAuthorizedToExecutor(_vault, _executor); } } diff --git a/contracts/prebuilts/unaudited/checkout/ICheckout.sol b/contracts/prebuilts/unaudited/checkout/ICheckout.sol index 582a29137..da9db32bc 100644 --- a/contracts/prebuilts/unaudited/checkout/ICheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/ICheckout.sol @@ -2,7 +2,18 @@ pragma solidity ^0.8.11; interface ICheckout { + /// @dev Emitted when an executor is authorized to use funds from the given vault. + event VaultAuthorizedToExecutor(address _vault, address _executor); + + /// @dev Emitted when a new Executor contract is deployed. + event ExecutorCreated(address _executor, address _defaultAdmin); + + /// @dev Emitted when a new Vault contrac is deployed. + event VaultCreated(address _vault, address _defaultAdmin); + function createVault(address _vaultAdmin, bytes32 _salt) external payable returns (address); function createExecutor(address _executorAdmin, bytes32 _salt) external payable returns (address); + + function authorizeVaultToExecutor(address _vault, address _executor) external; } diff --git a/contracts/prebuilts/unaudited/checkout/IVault.sol b/contracts/prebuilts/unaudited/checkout/IVault.sol index d924cdafa..dfd136b4d 100644 --- a/contracts/prebuilts/unaudited/checkout/IVault.sol +++ b/contracts/prebuilts/unaudited/checkout/IVault.sol @@ -11,6 +11,10 @@ interface IVault { /// @dev Emitted when executor contract withdraws tokens. event TokensTransferredToExecutor(address indexed _executor, address _token, uint256 _amount); + function deposit(address _token, uint256 _amount) external payable; + + function withdraw(address _token, uint256 _amount) external; + function transferTokensToExecutor(address _token, uint256 _amount) external; function setExecutor(address _executor) external; From ab7f9ffc172cebbdef3e00d568777056202cfcfc Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 9 Nov 2023 02:05:05 +0530 Subject: [PATCH 03/20] reorg --- contracts/prebuilts/unaudited/checkout/Checkout.sol | 2 +- contracts/prebuilts/unaudited/checkout/Executor.sol | 4 ++-- contracts/prebuilts/unaudited/checkout/Vault.sol | 2 +- .../unaudited/checkout/{ => interface}/ICheckout.sol | 0 .../unaudited/checkout/{ => interface}/IExecutor.sol | 0 .../prebuilts/unaudited/checkout/{ => interface}/IVault.sol | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename contracts/prebuilts/unaudited/checkout/{ => interface}/ICheckout.sol (100%) rename contracts/prebuilts/unaudited/checkout/{ => interface}/IExecutor.sol (100%) rename contracts/prebuilts/unaudited/checkout/{ => interface}/IVault.sol (100%) diff --git a/contracts/prebuilts/unaudited/checkout/Checkout.sol b/contracts/prebuilts/unaudited/checkout/Checkout.sol index dd69caa45..637b48986 100644 --- a/contracts/prebuilts/unaudited/checkout/Checkout.sol +++ b/contracts/prebuilts/unaudited/checkout/Checkout.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.11; import "@openzeppelin/contracts/proxy/Clones.sol"; -import "./ICheckout.sol"; +import "./interface/ICheckout.sol"; import "./Vault.sol"; import "./Executor.sol"; diff --git a/contracts/prebuilts/unaudited/checkout/Executor.sol b/contracts/prebuilts/unaudited/checkout/Executor.sol index c39d9ae93..591f96b15 100644 --- a/contracts/prebuilts/unaudited/checkout/Executor.sol +++ b/contracts/prebuilts/unaudited/checkout/Executor.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "./IExecutor.sol"; -import "./IVault.sol"; +import "./interface/IExecutor.sol"; +import "./interface/IVault.sol"; import "../../../lib/CurrencyTransferLib.sol"; import "../../../eip/interface/IERC20.sol"; diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index e6b20594e..19176c19d 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "./IVault.sol"; +import "./interface/IVault.sol"; import "../../../lib/CurrencyTransferLib.sol"; import "../../../eip/interface/IERC20.sol"; diff --git a/contracts/prebuilts/unaudited/checkout/ICheckout.sol b/contracts/prebuilts/unaudited/checkout/interface/ICheckout.sol similarity index 100% rename from contracts/prebuilts/unaudited/checkout/ICheckout.sol rename to contracts/prebuilts/unaudited/checkout/interface/ICheckout.sol diff --git a/contracts/prebuilts/unaudited/checkout/IExecutor.sol b/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol similarity index 100% rename from contracts/prebuilts/unaudited/checkout/IExecutor.sol rename to contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol diff --git a/contracts/prebuilts/unaudited/checkout/IVault.sol b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol similarity index 100% rename from contracts/prebuilts/unaudited/checkout/IVault.sol rename to contracts/prebuilts/unaudited/checkout/interface/IVault.sol From 24ea7e083447c5683fb3b14f72289c6403206265 Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 9 Nov 2023 04:22:29 +0530 Subject: [PATCH 04/20] update interface --- contracts/prebuilts/unaudited/checkout/Executor.sol | 3 +++ contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol | 2 ++ 2 files changed, 5 insertions(+) diff --git a/contracts/prebuilts/unaudited/checkout/Executor.sol b/contracts/prebuilts/unaudited/checkout/Executor.sol index 591f96b15..f2c3ec4e6 100644 --- a/contracts/prebuilts/unaudited/checkout/Executor.sol +++ b/contracts/prebuilts/unaudited/checkout/Executor.sol @@ -51,6 +51,9 @@ contract Executor is Initializable, PermissionsEnumerable, IExecutor { require(success, "Execution failed"); } + // TODO: rethink design and interface here + function swapAndExecute(UserOp calldata op) external {} + function _canExecute() internal view returns (bool) { return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); } diff --git a/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol b/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol index 1e41592dd..adfd5355a 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol @@ -27,4 +27,6 @@ interface IExecutor { } function execute(UserOp calldata op) external; + + function swapAndExecute(UserOp calldata op) external; // TODO: rethink design and interface here } From 5cc0d307427ebb4556f8e1e455819dbb1f490d3c Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 9 Nov 2023 20:00:26 +0530 Subject: [PATCH 05/20] setup swap functions --- .../prebuilts/unaudited/checkout/Executor.sol | 6 ++- .../prebuilts/unaudited/checkout/Vault.sol | 42 ++++++++++++++++++- .../checkout/interface/IExecutor.sol | 3 ++ .../unaudited/checkout/interface/IVault.sol | 2 + 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/Executor.sol b/contracts/prebuilts/unaudited/checkout/Executor.sol index f2c3ec4e6..0d606013d 100644 --- a/contracts/prebuilts/unaudited/checkout/Executor.sol +++ b/contracts/prebuilts/unaudited/checkout/Executor.sol @@ -34,7 +34,11 @@ contract Executor is Initializable, PermissionsEnumerable, IExecutor { require(_canExecute(), "Not authorized"); if (op.valueToSend != 0) { - IVault(op.vault).transferTokensToExecutor(op.currency, op.valueToSend); + if (op.swap) { + IVault(op.vault).swapAndTransferTokensToExecutor(op.currency, op.valueToSend); + } else { + IVault(op.vault).transferTokensToExecutor(op.currency, op.valueToSend); + } } bool success; diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index 19176c19d..272a68c03 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -28,7 +28,10 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { /// @dev Address of the Checkout entrypoint. address public checkout; - constructor() { + address public swapToken; + + constructor(address _swapToken) { + swapToken = _swapToken; _disableInitializers(); } @@ -83,16 +86,45 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { emit TokensTransferredToExecutor(msg.sender, _token, _amount); } + function swapAndTransferTokensToExecutor(address _token, uint256 _amount) external { + require(_canTransferTokens(), "Not authorized"); + + _swap(); + + uint256 balance = tokenBalance[_token]; + require(balance >= _amount, "Not enough balance"); + + tokenBalance[_token] -= _amount; + + CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); + + emit TokensTransferredToExecutor(msg.sender, _token, _amount); + } + + function swap() external { + require(_canSwap(), "Not authorized"); + + _swap(); + } + function setExecutor(address _executor) external { require(_canSetExecutor(), "Not authorized"); executor = _executor; } + function setSwapToken(address _swapToken) external { + require(_canSetSwapToken(), "Not authorized"); + + swapToken = _swapToken; + } + function canAuthorizeVaultToExecutor(address _expectedAdmin) external view returns (bool) { return hasRole(DEFAULT_ADMIN_ROLE, _expectedAdmin); } + function _swap() internal {} + function _canSetExecutor() internal view returns (bool) { return msg.sender == checkout; } @@ -100,4 +132,12 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { function _canTransferTokens() internal view returns (bool) { return msg.sender == executor; } + + function _canSwap() internal view returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function _canSetSwapToken() internal view returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + } } diff --git a/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol b/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol index adfd5355a..cf9f597a8 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol @@ -13,6 +13,8 @@ interface IExecutor { * * @param approvalRequired If need to approve erc20 to the target contract * + * @param swap If need to swap first + * * @param valueToSend Transaction value to send - both native and erc20 * * @param data Transaction calldata @@ -22,6 +24,7 @@ interface IExecutor { address currency; address vault; bool approvalRequired; + bool swap; uint256 valueToSend; bytes data; } diff --git a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol index dfd136b4d..e38ec5046 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol @@ -17,6 +17,8 @@ interface IVault { function transferTokensToExecutor(address _token, uint256 _amount) external; + function swapAndTransferTokensToExecutor(address _token, uint256 _amount) external; + function setExecutor(address _executor) external; function canAuthorizeVaultToExecutor(address _expectedAdmin) external view returns (bool); From e5d2b7bc84e312fcf5712eea366aa00d6b337742 Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 9 Nov 2023 20:06:44 +0530 Subject: [PATCH 06/20] comment --- .../prebuilts/unaudited/checkout/Vault.sol | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index 272a68c03..864c71678 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -40,6 +40,10 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); } + // ================================================= + // =============== Deposit and Withdraw ============ + // ================================================= + function deposit(address _token, uint256 _amount) external payable { require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized"); @@ -73,6 +77,10 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { emit TokensWithdrawn(_token, _amount); } + // ================================================= + // =============== Executor functions ============== + // ================================================= + function transferTokensToExecutor(address _token, uint256 _amount) external { require(_canTransferTokens(), "Not authorized"); @@ -101,12 +109,22 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { emit TokensTransferredToExecutor(msg.sender, _token, _amount); } + // ================================================= + // =============== Swap functionality ============== + // ================================================= + function swap() external { require(_canSwap(), "Not authorized"); _swap(); } + function _swap() internal {} + + // ================================================= + // =============== Setter functions ================ + // ================================================= + function setExecutor(address _executor) external { require(_canSetExecutor(), "Not authorized"); @@ -119,12 +137,14 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { swapToken = _swapToken; } + // ================================================= + // =============== Role checks ===================== + // ================================================= + function canAuthorizeVaultToExecutor(address _expectedAdmin) external view returns (bool) { return hasRole(DEFAULT_ADMIN_ROLE, _expectedAdmin); } - function _swap() internal {} - function _canSetExecutor() internal view returns (bool) { return msg.sender == checkout; } From 30a95906ca9edc96f59bda16c5cd2d3fd55c97d2 Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 10 Nov 2023 04:53:39 +0530 Subject: [PATCH 07/20] setup prototype tests --- .../prebuilts/unaudited/checkout/Executor.sol | 4 + .../prebuilts/unaudited/checkout/Vault.sol | 10 +- src/test/checkout/Prototype.t.sol | 92 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/test/checkout/Prototype.t.sol diff --git a/contracts/prebuilts/unaudited/checkout/Executor.sol b/contracts/prebuilts/unaudited/checkout/Executor.sol index 0d606013d..9b06e19fa 100644 --- a/contracts/prebuilts/unaudited/checkout/Executor.sol +++ b/contracts/prebuilts/unaudited/checkout/Executor.sol @@ -20,11 +20,15 @@ import "../../../extension/Initializable.sol"; // \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ contract Executor is Initializable, PermissionsEnumerable, IExecutor { + /// @dev Address of the Checkout entrypoint. + address public checkout; + constructor() { _disableInitializers(); } function initialize(address _defaultAdmin) external initializer { + checkout = msg.sender; _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); } diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index 864c71678..add93e0b5 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -28,7 +28,7 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { /// @dev Address of the Checkout entrypoint. address public checkout; - address public swapToken; + address public immutable swapToken; constructor(address _swapToken) { swapToken = _swapToken; @@ -131,11 +131,11 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { executor = _executor; } - function setSwapToken(address _swapToken) external { - require(_canSetSwapToken(), "Not authorized"); + // function setSwapToken(address _swapToken) external { + // require(_canSetSwapToken(), "Not authorized"); - swapToken = _swapToken; - } + // swapToken = _swapToken; + // } // ================================================= // =============== Role checks ===================== diff --git a/src/test/checkout/Prototype.t.sol b/src/test/checkout/Prototype.t.sol new file mode 100644 index 000000000..7e6675b99 --- /dev/null +++ b/src/test/checkout/Prototype.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../utils/BaseTest.sol"; + +import { Checkout, ICheckout } from "contracts/prebuilts/unaudited/checkout/Checkout.sol"; +import { Vault, IVault } from "contracts/prebuilts/unaudited/checkout/Vault.sol"; +import { Executor, IExecutor } from "contracts/prebuilts/unaudited/checkout/Checkout.sol"; + +contract CheckoutPrototypeTest is BaseTest { + address internal vaultImplementation; + address internal executorImplementation; + + Checkout internal checkout; + + Vault internal vaultOne; + Vault internal vaultTwo; + Executor internal executorOne; + Executor internal executorTwo; + + address internal vaultAdminOne; + address internal vaultAdminTwo; + address internal executorAdminOne; + address internal executorAdminTwo; + + DropERC721 internal targetDrop; + + MockERC20 internal mainCurrency; + MockERC20 internal altCurrencyOne; + MockERC20 internal altCurrencyTwo; + + function setClaimConditionCurrency(DropERC721 drop, address _currency) public { + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; + conditions[0].pricePerToken = 10; + conditions[0].currency = _currency; + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + } + + function setUp() public override { + super.setUp(); + + // setup actors + vaultAdminOne = getActor(1); + vaultAdminTwo = getActor(2); + executorAdminOne = getActor(3); + executorAdminTwo = getActor(4); + + // setup currencies + mainCurrency = new MockERC20(); + altCurrencyOne = new MockERC20(); + altCurrencyTwo = new MockERC20(); + + // setup target NFT Drop contract + targetDrop = DropERC721(getContract("DropERC721")); + vm.prank(deployer); + targetDrop.lazyMint(100, "ipfs://", ""); + setClaimConditionCurrency(targetDrop, address(mainCurrency)); + + // deploy vault and executor implementations + vaultImplementation = address(new Vault(address(mainCurrency))); + executorImplementation = address(new Executor()); + + // deploy checkout + checkout = new Checkout(deployer, vaultImplementation, executorImplementation); + } + + function test_checkout_createVault() public { + vaultOne = Vault(checkout.createVault(vaultAdminOne, "vaultAdminOne")); + + assertEq(vaultOne.checkout(), address(checkout)); + assertTrue(vaultOne.hasRole(bytes32(0x00), vaultAdminOne)); + + // should revert when deploying with same salt again + vm.expectRevert("ERC1167: create2 failed"); + checkout.createVault(vaultAdminOne, "vaultAdminOne"); + } + + function test_checkout_createExecutor() public { + executorOne = Executor(payable(checkout.createExecutor(executorAdminOne, "executorAdminOne"))); + + assertEq(executorOne.checkout(), address(checkout)); + assertTrue(executorOne.hasRole(bytes32(0x00), executorAdminOne)); + + // should revert when deploying with same salt again + vm.expectRevert("ERC1167: create2 failed"); + checkout.createExecutor(executorAdminOne, "executorAdminOne"); + } +} From 6d731710951cb747c332ae850950875d3ff7f03d Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 10 Nov 2023 05:53:55 +0530 Subject: [PATCH 08/20] more tests --- .../prebuilts/unaudited/checkout/Checkout.sol | 4 +- .../prebuilts/unaudited/checkout/Vault.sol | 3 + src/test/checkout/Prototype.t.sol | 89 +++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/Checkout.sol b/contracts/prebuilts/unaudited/checkout/Checkout.sol index 637b48986..a5c55cea4 100644 --- a/contracts/prebuilts/unaudited/checkout/Checkout.sol +++ b/contracts/prebuilts/unaudited/checkout/Checkout.sol @@ -22,10 +22,10 @@ import "../../../extension/PermissionsEnumerable.sol"; contract Checkout is PermissionsEnumerable, ICheckout { /// @dev Registry of vaults created through this Checkout - mapping(address => bool) isVaultRegistered; + mapping(address => bool) public isVaultRegistered; /// @dev Registry of executors created through this Checkout - mapping(address => bool) isExecutorRegistered; + mapping(address => bool) public isExecutorRegistered; address public immutable vaultImplementation; address public immutable executorImplementation; diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index add93e0b5..5d4464e90 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -127,6 +127,9 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { function setExecutor(address _executor) external { require(_canSetExecutor(), "Not authorized"); + if (_executor == executor) { + revert("Executor already set"); + } executor = _executor; } diff --git a/src/test/checkout/Prototype.t.sol b/src/test/checkout/Prototype.t.sol index 7e6675b99..4edbd6e74 100644 --- a/src/test/checkout/Prototype.t.sol +++ b/src/test/checkout/Prototype.t.sol @@ -6,6 +6,7 @@ import "../utils/BaseTest.sol"; import { Checkout, ICheckout } from "contracts/prebuilts/unaudited/checkout/Checkout.sol"; import { Vault, IVault } from "contracts/prebuilts/unaudited/checkout/Vault.sol"; import { Executor, IExecutor } from "contracts/prebuilts/unaudited/checkout/Checkout.sol"; +import { IDrop } from "contracts/extension/interface/IDrop.sol"; contract CheckoutPrototypeTest is BaseTest { address internal vaultImplementation; @@ -23,6 +24,8 @@ contract CheckoutPrototypeTest is BaseTest { address internal executorAdminOne; address internal executorAdminTwo; + address internal receiver; + DropERC721 internal targetDrop; MockERC20 internal mainCurrency; @@ -48,12 +51,21 @@ contract CheckoutPrototypeTest is BaseTest { vaultAdminTwo = getActor(2); executorAdminOne = getActor(3); executorAdminTwo = getActor(4); + receiver = getActor(5); // setup currencies mainCurrency = new MockERC20(); altCurrencyOne = new MockERC20(); altCurrencyTwo = new MockERC20(); + // mint and approve currencies + mainCurrency.mint(address(vaultAdminOne), 100 ether); + altCurrencyOne.mint(address(vaultAdminOne), 100 ether); + altCurrencyTwo.mint(address(vaultAdminOne), 100 ether); + mainCurrency.mint(address(vaultAdminTwo), 100 ether); + altCurrencyOne.mint(address(vaultAdminTwo), 100 ether); + altCurrencyTwo.mint(address(vaultAdminTwo), 100 ether); + // setup target NFT Drop contract targetDrop = DropERC721(getContract("DropERC721")); vm.prank(deployer); @@ -73,6 +85,7 @@ contract CheckoutPrototypeTest is BaseTest { assertEq(vaultOne.checkout(), address(checkout)); assertTrue(vaultOne.hasRole(bytes32(0x00), vaultAdminOne)); + assertTrue(checkout.isVaultRegistered(address(vaultOne))); // should revert when deploying with same salt again vm.expectRevert("ERC1167: create2 failed"); @@ -84,9 +97,85 @@ contract CheckoutPrototypeTest is BaseTest { assertEq(executorOne.checkout(), address(checkout)); assertTrue(executorOne.hasRole(bytes32(0x00), executorAdminOne)); + assertTrue(checkout.isExecutorRegistered(address(executorOne))); // should revert when deploying with same salt again vm.expectRevert("ERC1167: create2 failed"); checkout.createExecutor(executorAdminOne, "executorAdminOne"); } + + function test_checkout_authorizeVaultToExecutor() public { + vaultOne = Vault(checkout.createVault(vaultAdminOne, "vaultAdminOne")); + executorOne = Executor(payable(checkout.createExecutor(executorAdminOne, "executorAdminOne"))); + + vm.prank(vaultAdminOne); + checkout.authorizeVaultToExecutor(address(vaultOne), address(executorOne)); + assertEq(vaultOne.executor(), address(executorOne)); + + // revert for unauthorized caller + vm.prank(address(0x123)); + vm.expectRevert("Not authorized"); + checkout.authorizeVaultToExecutor(address(vaultOne), address(executorOne)); + + // revert for unknown executor + vm.prank(vaultAdminOne); + vm.expectRevert("Executor not found"); + checkout.authorizeVaultToExecutor(address(vaultOne), address(0x123)); + } + + function test_executor_executeOp() public { + // setup contracts + vaultOne = Vault(checkout.createVault(vaultAdminOne, "vaultAdminOne")); + executorOne = Executor(payable(checkout.createExecutor(executorAdminOne, "executorAdminOne"))); + + vm.prank(vaultAdminOne); + checkout.authorizeVaultToExecutor(address(vaultOne), address(executorOne)); + + // deposit currencies in vault + vm.startPrank(address(vaultAdminOne)); + mainCurrency.approve(address(vaultOne), type(uint256).max); + vaultOne.deposit(address(mainCurrency), 10 ether); + vm.stopPrank(); + + // create user op -- claim tokens on targetDrop + uint256 _quantityToClaim = 5; + uint256 _totalPrice = 5 * 10; // claim condition price is set as 10 above in setup + DropERC721.AllowlistProof memory alp; + bytes memory callData = abi.encodeWithSelector( + IDrop.claim.selector, + receiver, + _quantityToClaim, + address(mainCurrency), + 10, + alp, + "" + ); + IExecutor.UserOp memory op = IExecutor.UserOp({ + target: address(targetDrop), + currency: address(mainCurrency), + vault: address(vaultOne), + approvalRequired: true, + swap: false, + valueToSend: _totalPrice, + data: callData + }); + + // check state before + assertEq(targetDrop.balanceOf(receiver), 0); + assertEq(targetDrop.nextTokenIdToClaim(), 0); + assertEq(mainCurrency.balanceOf(address(vaultOne)), 10 ether); + assertEq(mainCurrency.balanceOf(address(saleRecipient)), 0); + assertEq(mainCurrency.allowance(address(vaultOne), address(executorOne)), 0); + + // execute + vm.prank(executorAdminOne); + executorOne.execute(op); + + // check state after + assertEq(targetDrop.balanceOf(receiver), _quantityToClaim); + assertEq(targetDrop.nextTokenIdToClaim(), _quantityToClaim); + assertEq(mainCurrency.balanceOf(address(vaultOne)), 10 ether - _totalPrice); + assertEq(mainCurrency.balanceOf(address(saleRecipient)), _totalPrice - (_totalPrice * platformFeeBps) / 10_000); + assertEq(mainCurrency.allowance(address(vaultOne), address(executorOne)), 0); + } } From 901536d6c8cfa738bf7be665f5e2f00fefff9852 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 21 Nov 2023 01:04:26 +0530 Subject: [PATCH 09/20] swap --- .../prebuilts/unaudited/checkout/Executor.sol | 11 ++-- .../prebuilts/unaudited/checkout/Vault.sol | 52 ++++++++++++++----- .../checkout/interface/IExecutor.sol | 7 +-- .../unaudited/checkout/interface/ISwap.sol | 11 ++++ .../unaudited/checkout/interface/IVault.sol | 10 +++- src/test/checkout/Prototype.t.sol | 3 +- 6 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 contracts/prebuilts/unaudited/checkout/interface/ISwap.sol diff --git a/contracts/prebuilts/unaudited/checkout/Executor.sol b/contracts/prebuilts/unaudited/checkout/Executor.sol index 9b06e19fa..f255a3351 100644 --- a/contracts/prebuilts/unaudited/checkout/Executor.sol +++ b/contracts/prebuilts/unaudited/checkout/Executor.sol @@ -38,11 +38,7 @@ contract Executor is Initializable, PermissionsEnumerable, IExecutor { require(_canExecute(), "Not authorized"); if (op.valueToSend != 0) { - if (op.swap) { - IVault(op.vault).swapAndTransferTokensToExecutor(op.currency, op.valueToSend); - } else { - IVault(op.vault).transferTokensToExecutor(op.currency, op.valueToSend); - } + IVault(op.vault).transferTokensToExecutor(op.currency, op.valueToSend); } bool success; @@ -60,7 +56,10 @@ contract Executor is Initializable, PermissionsEnumerable, IExecutor { } // TODO: rethink design and interface here - function swapAndExecute(UserOp calldata op) external {} + function swapAndExecute(UserOp calldata op, SwapOp calldata swap) external { + require(_canExecute(), "Not authorized"); + IVault(op.vault).swapAndTransferTokensToExecutor(op.currency, op.valueToSend, swap); + } function _canExecute() internal view returns (bool) { return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index 5d4464e90..5ace472de 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -28,10 +28,10 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { /// @dev Address of the Checkout entrypoint. address public checkout; - address public immutable swapToken; + address public swapToken; + address public swapRouter; - constructor(address _swapToken) { - swapToken = _swapToken; + constructor() { _disableInitializers(); } @@ -94,10 +94,14 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { emit TokensTransferredToExecutor(msg.sender, _token, _amount); } - function swapAndTransferTokensToExecutor(address _token, uint256 _amount) external { + function swapAndTransferTokensToExecutor( + address _token, + uint256 _amount, + SwapOp memory _swapOp + ) external { require(_canTransferTokens(), "Not authorized"); - _swap(); + _swap(_swapOp); uint256 balance = tokenBalance[_token]; require(balance >= _amount, "Not enough balance"); @@ -113,13 +117,26 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { // =============== Swap functionality ============== // ================================================= - function swap() external { + function swap(SwapOp memory _swapOp) external { require(_canSwap(), "Not authorized"); - _swap(); + _swap(_swapOp); } - function _swap() internal {} + function _swap(SwapOp memory _swapOp) internal { + address _swapRouter = swapRouter; + uint256 balanceBefore = IERC20(_swapOp.tokenIn).balanceOf(address(this)); + + IERC20(_swapOp.tokenOut).approve(_swapRouter, type(uint256).max); + (bool success, ) = _swapRouter.call(_swapOp.swapCalldata); + require(success, "Swap failed"); + IERC20(_swapOp.tokenOut).approve(_swapRouter, 0); + + tokenBalance[_swapOp.tokenOut] = IERC20(_swapOp.tokenOut).balanceOf(address(this)); + + uint256 balanceAfter = IERC20(_swapOp.tokenIn).balanceOf(address(this)); + require(_swapOp.amountIn == (balanceAfter - balanceBefore), "Incorrect amount received from swap"); + } // ================================================= // =============== Setter functions ================ @@ -134,11 +151,18 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { executor = _executor; } - // function setSwapToken(address _swapToken) external { - // require(_canSetSwapToken(), "Not authorized"); + function setSwapToken(address _swapToken) external { + require(_canSetSwap(), "Not authorized"); - // swapToken = _swapToken; - // } + swapToken = _swapToken; + } + + function setSwapRouter(address _swapRouter) external { + require(_canSetSwap(), "Not authorized"); + require(_swapRouter != address(0), "Zero address"); + + swapRouter = _swapRouter; + } // ================================================= // =============== Role checks ===================== @@ -157,10 +181,10 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { } function _canSwap() internal view returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender) || msg.sender == executor; } - function _canSetSwapToken() internal view returns (bool) { + function _canSetSwap() internal view returns (bool) { return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); } } diff --git a/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol b/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol index cf9f597a8..d6500bf0c 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/IExecutor.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -interface IExecutor { +import "./ISwap.sol"; + +interface IExecutor is ISwap { /** * @notice Details of the transaction to execute on target contract. * @@ -24,12 +26,11 @@ interface IExecutor { address currency; address vault; bool approvalRequired; - bool swap; uint256 valueToSend; bytes data; } function execute(UserOp calldata op) external; - function swapAndExecute(UserOp calldata op) external; // TODO: rethink design and interface here + function swapAndExecute(UserOp calldata op, SwapOp memory swapOp) external; } diff --git a/contracts/prebuilts/unaudited/checkout/interface/ISwap.sol b/contracts/prebuilts/unaudited/checkout/interface/ISwap.sol new file mode 100644 index 000000000..ef9fcd5ef --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/interface/ISwap.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface ISwap { + struct SwapOp { + address tokenOut; + address tokenIn; + uint256 amountIn; + bytes swapCalldata; + } +} diff --git a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol index e38ec5046..532394b23 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -interface IVault { +import "./ISwap.sol"; + +interface IVault is ISwap { /// @dev Emitted when contract admin withdraws tokens. event TokensWithdrawn(address _token, uint256 _amount); @@ -17,7 +19,11 @@ interface IVault { function transferTokensToExecutor(address _token, uint256 _amount) external; - function swapAndTransferTokensToExecutor(address _token, uint256 _amount) external; + function swapAndTransferTokensToExecutor( + address _token, + uint256 _amount, + SwapOp memory _swapOp + ) external; function setExecutor(address _executor) external; diff --git a/src/test/checkout/Prototype.t.sol b/src/test/checkout/Prototype.t.sol index 4edbd6e74..f9d7de843 100644 --- a/src/test/checkout/Prototype.t.sol +++ b/src/test/checkout/Prototype.t.sol @@ -73,7 +73,7 @@ contract CheckoutPrototypeTest is BaseTest { setClaimConditionCurrency(targetDrop, address(mainCurrency)); // deploy vault and executor implementations - vaultImplementation = address(new Vault(address(mainCurrency))); + vaultImplementation = address(new Vault()); executorImplementation = address(new Executor()); // deploy checkout @@ -155,7 +155,6 @@ contract CheckoutPrototypeTest is BaseTest { currency: address(mainCurrency), vault: address(vaultOne), approvalRequired: true, - swap: false, valueToSend: _totalPrice, data: callData }); From 567f8847720d986c1c24c04c9795fd1f839236c0 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 21 Nov 2023 05:10:01 +0530 Subject: [PATCH 10/20] fix swap --- .../prebuilts/unaudited/checkout/Vault.sol | 89 +++++++------------ .../unaudited/checkout/interface/ISwap.sol | 3 + .../unaudited/checkout/interface/IVault.sol | 7 +- src/test/checkout/Map.t.sol | 55 ++++++++++++ src/test/checkout/Prototype.t.sol | 3 +- 5 files changed, 95 insertions(+), 62 deletions(-) create mode 100644 src/test/checkout/Map.t.sol diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index 5ace472de..c0a5ec8c9 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -19,17 +19,13 @@ import "../../../extension/Initializable.sol"; // \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ contract Vault is Initializable, PermissionsEnumerable, IVault { - /// @dev Mapping from token address to total balance in the vault. - mapping(address => uint256) public tokenBalance; - /// @dev Address of the executor for this vault. address public executor; /// @dev Address of the Checkout entrypoint. address public checkout; - address public swapToken; - address public swapRouter; + mapping(address => bool) public isApprovedRouter; constructor() { _disableInitializers(); @@ -41,37 +37,12 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { } // ================================================= - // =============== Deposit and Withdraw ============ + // =============== Withdraw ======================== // ================================================= - function deposit(address _token, uint256 _amount) external payable { - require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized"); - - uint256 _actualAmount; - - if (_token == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == _amount, "!Amount"); - _actualAmount = _amount; - - tokenBalance[_token] += _actualAmount; - } else { - uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); - CurrencyTransferLib.safeTransferERC20(_token, msg.sender, address(this), _amount); - _actualAmount = IERC20(_token).balanceOf(address(this)) - balanceBefore; - - tokenBalance[_token] += _actualAmount; - } - - emit TokensDeposited(_token, _actualAmount); - } - function withdraw(address _token, uint256 _amount) external { require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized"); - uint256 balance = tokenBalance[_token]; - // to prevent locking of direct-transferred tokens - tokenBalance[_token] = _amount > balance ? 0 : balance - _amount; - CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); emit TokensWithdrawn(_token, _amount); @@ -84,10 +55,11 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { function transferTokensToExecutor(address _token, uint256 _amount) external { require(_canTransferTokens(), "Not authorized"); - uint256 balance = tokenBalance[_token]; - require(balance >= _amount, "Not enough balance"); + uint256 balance = _token == CurrencyTransferLib.NATIVE_TOKEN + ? address(this).balance + : IERC20(_token).balanceOf(address(this)); - tokenBalance[_token] -= _amount; + require(balance >= _amount, "Not enough balance"); CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); @@ -100,13 +72,15 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { SwapOp memory _swapOp ) external { require(_canTransferTokens(), "Not authorized"); + require(isApprovedRouter[_swapOp.router], "Invalid router address"); _swap(_swapOp); - uint256 balance = tokenBalance[_token]; - require(balance >= _amount, "Not enough balance"); + uint256 balance = _token == CurrencyTransferLib.NATIVE_TOKEN + ? address(this).balance + : IERC20(_token).balanceOf(address(this)); - tokenBalance[_token] -= _amount; + require(balance >= _amount, "Not enough balance"); CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); @@ -124,18 +98,29 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { } function _swap(SwapOp memory _swapOp) internal { - address _swapRouter = swapRouter; - uint256 balanceBefore = IERC20(_swapOp.tokenIn).balanceOf(address(this)); + address _tokenOut = _swapOp.tokenOut; + address _tokenIn = _swapOp.tokenIn; + address _router = _swapOp.router; - IERC20(_swapOp.tokenOut).approve(_swapRouter, type(uint256).max); - (bool success, ) = _swapRouter.call(_swapOp.swapCalldata); - require(success, "Swap failed"); - IERC20(_swapOp.tokenOut).approve(_swapRouter, 0); + // get quote for amountIn + (, bytes memory quoteData) = _router.staticcall(_swapOp.quoteCalldata); + uint256 amountIn; + uint256 offset = _swapOp.amountInOffset; - tokenBalance[_swapOp.tokenOut] = IERC20(_swapOp.tokenOut).balanceOf(address(this)); + assembly { + amountIn := mload(add(add(quoteData, 32), offset)) + } + + // perform swap + bool success; + if (_tokenIn == CurrencyTransferLib.NATIVE_TOKEN) { + (success, ) = _router.call{ value: amountIn }(_swapOp.swapCalldata); + } else { + IERC20(_tokenIn).approve(_swapOp.router, amountIn); + (success, ) = _router.call(_swapOp.swapCalldata); + } - uint256 balanceAfter = IERC20(_swapOp.tokenIn).balanceOf(address(this)); - require(_swapOp.amountIn == (balanceAfter - balanceBefore), "Incorrect amount received from swap"); + require(success, "Swap failed"); } // ================================================= @@ -151,17 +136,11 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { executor = _executor; } - function setSwapToken(address _swapToken) external { - require(_canSetSwap(), "Not authorized"); - - swapToken = _swapToken; - } - - function setSwapRouter(address _swapRouter) external { + function approveSwapRouter(address _swapRouter, bool _toApprove) external { require(_canSetSwap(), "Not authorized"); require(_swapRouter != address(0), "Zero address"); - swapRouter = _swapRouter; + isApprovedRouter[_swapRouter] = _toApprove; } // ================================================= @@ -181,7 +160,7 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { } function _canSwap() internal view returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, msg.sender) || msg.sender == executor; + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); } function _canSetSwap() internal view returns (bool) { diff --git a/contracts/prebuilts/unaudited/checkout/interface/ISwap.sol b/contracts/prebuilts/unaudited/checkout/interface/ISwap.sol index ef9fcd5ef..523b0a049 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/ISwap.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/ISwap.sol @@ -3,9 +3,12 @@ pragma solidity ^0.8.11; interface ISwap { struct SwapOp { + address router; address tokenOut; address tokenIn; uint256 amountIn; + uint256 amountInOffset; bytes swapCalldata; + bytes quoteCalldata; } } diff --git a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol index 532394b23..c1dcae123 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol @@ -7,14 +7,9 @@ interface IVault is ISwap { /// @dev Emitted when contract admin withdraws tokens. event TokensWithdrawn(address _token, uint256 _amount); - /// @dev Emitted when contract admin deposits tokens. - event TokensDeposited(address _token, uint256 _amount); - /// @dev Emitted when executor contract withdraws tokens. event TokensTransferredToExecutor(address indexed _executor, address _token, uint256 _amount); - function deposit(address _token, uint256 _amount) external payable; - function withdraw(address _token, uint256 _amount) external; function transferTokensToExecutor(address _token, uint256 _amount) external; @@ -27,5 +22,7 @@ interface IVault is ISwap { function setExecutor(address _executor) external; + function approveSwapRouter(address _swapRouter, bool _toApprove) external; + function canAuthorizeVaultToExecutor(address _expectedAdmin) external view returns (bool); } diff --git a/src/test/checkout/Map.t.sol b/src/test/checkout/Map.t.sol new file mode 100644 index 000000000..8897087fd --- /dev/null +++ b/src/test/checkout/Map.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../utils/BaseTest.sol"; + +interface IRandom { + function random() external view returns (uint256); +} + +contract MapCheck { + struct S { + address addr; + } + uint256 public a; + mapping(bytes4 => S) public myMap; + + constructor() { + a = 10; + myMap[IRandom.random.selector] = S({ addr: address(123) }); + } + + function checkA() public view returns (uint256 res) { + assembly { + res := sload(0) + } + } + + function checkMap() public view returns (uint256 res) { + bytes4 sel = IRandom.random.selector; + assembly { + mstore(0, sel) + + mstore(32, myMap.slot) + + let hash := keccak256(0, 64) + res := myMap.slot + } + } +} + +contract MapCheckTest { + MapCheck internal c; + + function setUp() public { + c = new MapCheck(); + } + + function test_mappingSlot() public { + console.log(c.myMap(IRandom.random.selector)); + console.logBytes4(IRandom.random.selector); + + console.log(c.checkA()); + console.log(c.checkMap()); + } +} diff --git a/src/test/checkout/Prototype.t.sol b/src/test/checkout/Prototype.t.sol index f9d7de843..844d96801 100644 --- a/src/test/checkout/Prototype.t.sol +++ b/src/test/checkout/Prototype.t.sol @@ -133,8 +133,7 @@ contract CheckoutPrototypeTest is BaseTest { // deposit currencies in vault vm.startPrank(address(vaultAdminOne)); - mainCurrency.approve(address(vaultOne), type(uint256).max); - vaultOne.deposit(address(mainCurrency), 10 ether); + mainCurrency.transfer(address(vaultOne), 10 ether); vm.stopPrank(); // create user op -- claim tokens on targetDrop From 6da7d87ed67dc75b6ca8ac7611c81d7189c57a91 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 13 Dec 2023 03:12:19 +0530 Subject: [PATCH 11/20] lint --- contracts/prebuilts/unaudited/checkout/Vault.sol | 7 +------ lib/chainlink | 2 +- lib/ds-test | 2 +- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- lib/openzeppelin-contracts-upgradeable | 2 +- 6 files changed, 6 insertions(+), 11 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/Vault.sol b/contracts/prebuilts/unaudited/checkout/Vault.sol index c0a5ec8c9..1070d5334 100644 --- a/contracts/prebuilts/unaudited/checkout/Vault.sol +++ b/contracts/prebuilts/unaudited/checkout/Vault.sol @@ -66,11 +66,7 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { emit TokensTransferredToExecutor(msg.sender, _token, _amount); } - function swapAndTransferTokensToExecutor( - address _token, - uint256 _amount, - SwapOp memory _swapOp - ) external { + function swapAndTransferTokensToExecutor(address _token, uint256 _amount, SwapOp memory _swapOp) external { require(_canTransferTokens(), "Not authorized"); require(isApprovedRouter[_swapOp.router], "Invalid router address"); @@ -98,7 +94,6 @@ contract Vault is Initializable, PermissionsEnumerable, IVault { } function _swap(SwapOp memory _swapOp) internal { - address _tokenOut = _swapOp.tokenOut; address _tokenIn = _swapOp.tokenIn; address _router = _swapOp.router; diff --git a/lib/chainlink b/lib/chainlink index 9d5ec20aa..5d44bd4e8 160000 --- a/lib/chainlink +++ b/lib/chainlink @@ -1 +1 @@ -Subproject commit 9d5ec20aa7c03c5f08722fa88f621075d300dcc1 +Subproject commit 5d44bd4e8fa2bdc80228a0df891960d72246b645 diff --git a/lib/ds-test b/lib/ds-test index 6da7dd8f7..e282159d5 160000 --- a/lib/ds-test +++ b/lib/ds-test @@ -1 +1 @@ -Subproject commit 6da7dd8f7395f83e1fb6fa88a64ba9a030f85d4f +Subproject commit e282159d5170298eb2455a6c05280ab5a73a4ef0 diff --git a/lib/forge-std b/lib/forge-std index c0c6a4206..2f1126975 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit c0c6a4206531a3f785538240412ea2467ef58ebf +Subproject commit 2f112697506eab12d433a65fdc31a639548fe365 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index ecd2ca2cd..fd81a96f0 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d +Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 0a2cb9a44..3d4c0d574 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 0a2cb9a445c365870ed7a8ab461b12acf3e27d63 +Subproject commit 3d4c0d5741b131c231e558d7a6213392ab3672a5 From 7fae58540bd05dbb7260925861a646233430bbb2 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 8 Jan 2024 23:43:08 +0530 Subject: [PATCH 12/20] checkout plugin --- .../checkout/prb/IPRBProxyPlugin.sol | 19 ++++++ .../unaudited/checkout/prb/PluginCheckout.sol | 33 +++++++++ .../unaudited/checkout/prb/TargetCheckout.sol | 67 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol create mode 100644 contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol create mode 100644 contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol diff --git a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol new file mode 100644 index 000000000..fdafa4852 --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4; + +/// @title IPRBProxyPlugin +/// @notice Interface for plugin contracts that can be installed on a proxy. +/// @dev Plugins are contracts that enable the proxy to interact with and respond to calls from other contracts. These +/// plugins are run via the proxy's fallback function. +/// +/// This interface is meant to be directly inherited by plugin implementations. +interface IPRBProxyPlugin { + /// @notice Retrieves the methods implemented by the plugin. + /// @dev The registry pulls these methods when installing the plugin. + /// + /// Requirements: + /// - The plugin must implement at least one method. + /// + /// @return methods The array of the methods implemented by the plugin. + function getMethods() external returns (bytes4[] memory methods); +} diff --git a/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol new file mode 100644 index 000000000..7d68e0ac8 --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +import { IPRBProxyPlugin } from "./IPRBProxyPlugin.sol"; + +import { TargetCheckout } from "./TargetCheckout.sol"; + +contract PluginCheckout is IPRBProxyPlugin, TargetCheckout { + function getMethods() external pure override returns (bytes4[] memory) { + bytes4[] memory methods = new bytes4[](11); + methods[0] = this.withdraw.selector; + methods[1] = this.hasRole.selector; + methods[2] = this.getRoleAdmin.selector; + methods[3] = this.grantRole.selector; + methods[4] = this.revokeRole.selector; + methods[5] = this.renounceRole.selector; + methods[6] = this.DEFAULT_ADMIN_ROLE.selector; + methods[7] = this.getRoleMember.selector; + methods[8] = this.getRoleMemberCount.selector; + methods[9] = this.execute.selector; + methods[10] = this.swapAndExecute.selector; + return methods; + } +} diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol new file mode 100644 index 000000000..9e799fc3b --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../interface/IExecutor.sol"; +import "../interface/IVault.sol"; + +import "../../../../lib/CurrencyTransferLib.sol"; +import "../../../../eip/interface/IERC20.sol"; + +import "../../../../extension/PermissionsEnumerable.sol"; +import "../../../../extension/Initializable.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +contract TargetCheckout is Initializable, PermissionsEnumerable, IExecutor { + function initialize(address _defaultAdmin) external initializer { + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + } + + // ================================================= + // =============== Withdraw ======================== + // ================================================= + + function withdraw(address _token, uint256 _amount) external { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized"); + + CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); + } + + // ================================================= + // =============== Executor functions ============== + // ================================================= + + function execute(UserOp calldata op) external { + require(_canExecute(), "Not authorized"); + + bool success; + if (op.currency == CurrencyTransferLib.NATIVE_TOKEN) { + (success, ) = op.target.call{ value: op.valueToSend }(op.data); + } else { + if (op.valueToSend != 0 && op.approvalRequired) { + IERC20(op.currency).approve(op.target, op.valueToSend); + } + + (success, ) = op.target.call(op.data); + } + + require(success, "Execution failed"); + } + + function swapAndExecute(UserOp calldata op, SwapOp calldata swap) external { + // require(_canExecute(), "Not authorized"); + // TODO: Perform swap and execute here + } + + // TODO: Re-evaluate utility of this function + function _canExecute() internal view returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); + } +} From 30a12a06612bebbfcdae16e99354de8c60b31f7c Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 8 Jan 2024 23:49:01 +0530 Subject: [PATCH 13/20] prettier --- contracts/prebuilts/unaudited/checkout/Checkout.sol | 6 +----- contracts/prebuilts/unaudited/checkout/interface/IVault.sol | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/Checkout.sol b/contracts/prebuilts/unaudited/checkout/Checkout.sol index a5c55cea4..02151c198 100644 --- a/contracts/prebuilts/unaudited/checkout/Checkout.sol +++ b/contracts/prebuilts/unaudited/checkout/Checkout.sol @@ -30,11 +30,7 @@ contract Checkout is PermissionsEnumerable, ICheckout { address public immutable vaultImplementation; address public immutable executorImplementation; - constructor( - address _defaultAdmin, - address _vaultImplementation, - address _executorImplementation - ) { + constructor(address _defaultAdmin, address _vaultImplementation, address _executorImplementation) { vaultImplementation = _vaultImplementation; executorImplementation = _executorImplementation; diff --git a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol index c1dcae123..23c26ef38 100644 --- a/contracts/prebuilts/unaudited/checkout/interface/IVault.sol +++ b/contracts/prebuilts/unaudited/checkout/interface/IVault.sol @@ -14,11 +14,7 @@ interface IVault is ISwap { function transferTokensToExecutor(address _token, uint256 _amount) external; - function swapAndTransferTokensToExecutor( - address _token, - uint256 _amount, - SwapOp memory _swapOp - ) external; + function swapAndTransferTokensToExecutor(address _token, uint256 _amount, SwapOp memory _swapOp) external; function setExecutor(address _executor) external; From c0eb3f2d0eb5f6a495de2956070d08750b1ecff1 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 9 Jan 2024 21:08:26 +0530 Subject: [PATCH 14/20] remove PermissionsEnumerable --- .../unaudited/checkout/prb/IPRBProxy.sol | 66 +++++ .../checkout/prb/IPRBProxyRegistry.sol | 267 ++++++++++++++++++ .../unaudited/checkout/prb/PluginCheckout.sol | 14 +- .../unaudited/checkout/prb/TargetCheckout.sol | 19 +- 4 files changed, 339 insertions(+), 27 deletions(-) create mode 100644 contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol create mode 100644 contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol diff --git a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol new file mode 100644 index 000000000..4e4e83b87 --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4; + +import { IPRBProxyPlugin } from "./IPRBProxyPlugin.sol"; +import { IPRBProxyRegistry } from "./IPRBProxyRegistry.sol"; + +/// @title IPRBProxy +/// @notice Proxy contract to compose transactions on behalf of the owner. +interface IPRBProxy { + /*////////////////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when a target contract reverts without a specified reason. + error PRBProxy_ExecutionReverted(); + + /// @notice Thrown when an unauthorized account tries to execute a delegate call. + error PRBProxy_ExecutionUnauthorized(address owner, address caller, address target); + + /// @notice Thrown when the fallback function fails to find an installed plugin for the method selector. + error PRBProxy_PluginNotInstalledForMethod(address caller, address owner, bytes4 method); + + /// @notice Thrown when a plugin execution reverts without a specified reason. + error PRBProxy_PluginReverted(IPRBProxyPlugin plugin); + + /// @notice Thrown when a non-contract address is passed as the target. + error PRBProxy_TargetNotContract(address target); + + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a target contract is delegate called. + event Execute(address indexed target, bytes data, bytes response); + + /// @notice Emitted when a plugin is run for a provided method. + event RunPlugin(IPRBProxyPlugin indexed plugin, bytes data, bytes response); + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice The address of the owner account or contract, which controls the proxy. + function owner() external view returns (address); + + /// @notice The address of the registry that has deployed this proxy. + function registry() external view returns (IPRBProxyRegistry); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Delegate calls to the provided target contract by forwarding the data. It returns the data it + /// gets back, and bubbles up any potential revert. + /// + /// @dev Emits an {Execute} event. + /// + /// Requirements: + /// - The caller must be either the owner or an envoy with permission. + /// - `target` must be a contract. + /// + /// @param target The address of the target contract. + /// @param data Function selector plus ABI encoded data. + /// @return response The response received from the target contract, if any. + function execute(address target, bytes calldata data) external payable returns (bytes memory response); +} diff --git a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol new file mode 100644 index 000000000..cc6405751 --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4; + +import { IPRBProxy } from "./IPRBProxy.sol"; +import { IPRBProxyPlugin } from "./IPRBProxyPlugin.sol"; + +/// @title IPRBProxyRegistry +/// @notice Deploys new proxies via CREATE2 and keeps a registry of owners to proxies. Proxies can only be deployed +/// once per owner, and they cannot be transferred. The registry also supports installing plugins, which are used +/// for extending the functionality of the proxy. +interface IPRBProxyRegistry { + /*////////////////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when trying to install a plugin that implements a method already implemented by another + /// installed plugin. + error PRBProxyRegistry_PluginMethodCollision( + IPRBProxyPlugin currentPlugin, IPRBProxyPlugin newPlugin, bytes4 method + ); + + /// @notice Thrown when trying to uninstall an unknown plugin. + error PRBProxyRegistry_PluginUnknown(IPRBProxyPlugin plugin); + + /// @notice Thrown when trying to install a plugin that doesn't implement any method. + error PRBProxyRegistry_PluginWithZeroMethods(IPRBProxyPlugin plugin); + + /// @notice Thrown when a function requires the user to have a proxy. + error PRBProxyRegistry_UserDoesNotHaveProxy(address user); + + /// @notice Thrown when a function requires the user to not have a proxy. + error PRBProxyRegistry_UserHasProxy(address user, IPRBProxy proxy); + + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new proxy is deployed. + event DeployProxy(address indexed operator, address indexed owner, IPRBProxy proxy); + + /// @notice Emitted when a plugin is installed. + event InstallPlugin( + address indexed owner, IPRBProxy indexed proxy, IPRBProxyPlugin indexed plugin, bytes4[] methods + ); + + /// @notice Emitted when an envoy's permission is updated. + event SetPermission( + address indexed owner, IPRBProxy indexed proxy, address indexed envoy, address target, bool newPermission + ); + + /// @notice Emitted when a plugin is uninstalled. + event UninstallPlugin( + address indexed owner, IPRBProxy indexed proxy, IPRBProxyPlugin indexed plugin, bytes4[] methods + ); + + /*////////////////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @param owner The address of the user who will own the proxy. + /// @param target The address of the target to delegate call to. Can be set to zero. + /// @param data The call data to be passed to the target. Can be set to zero. + struct ConstructorParams { + address owner; + address target; + bytes data; + } + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice The release version of the proxy system, which applies to both the registry and deployed proxies. + /// @dev This is stored in the registry rather than the proxy to save gas for end users. + function VERSION() external view returns (string memory); + + /// @notice The parameters used in constructing the proxy, which the registry sets transiently during proxy + /// deployment. + /// @dev The proxy constructor fetches these parameters. + function constructorParams() external view returns (address owner, address target, bytes memory data); + + /// @notice Retrieves the list of installed methods for the provided plugin. + /// @dev An empty array is returned if the plugin is unknown. + /// @param owner The proxy owner for the query. + /// @param plugin The plugin for the query. + function getMethodsByOwner(address owner, IPRBProxyPlugin plugin) external view returns (bytes4[] memory methods); + + /// @notice Retrieves the list of installed methods for the provided plugin. + /// @dev An empty array is returned if the plugin is unknown. + /// @param proxy The proxy for the query. + /// @param plugin The plugin for the query. + function getMethodsByProxy( + IPRBProxy proxy, + IPRBProxyPlugin plugin + ) + external + view + returns (bytes4[] memory methods); + + /// @notice Retrieves a boolean flag that indicates whether the provided envoy has permission to call the provided + /// target. + /// @param owner The proxy owner for the query. + /// @param envoy The address checked for permission to call the target. + /// @param target The address of the target. + function getPermissionByOwner( + address owner, + address envoy, + address target + ) + external + view + returns (bool permission); + + /// @notice Retrieves a boolean flag that indicates whether the provided envoy has permission to call the provided + /// target. + /// @param proxy The proxy for the query. + /// @param envoy The address checked for permission to call the target. + /// @param target The address of the target. + function getPermissionByProxy( + IPRBProxy proxy, + address envoy, + address target + ) + external + view + returns (bool permission); + + /// @notice Retrieves the address of the plugin installed for the provided method selector. + /// @dev The zero address is returned if no plugin is installed. + /// @param owner The proxy owner for the query. + /// @param method The method selector for the query. + function getPluginByOwner(address owner, bytes4 method) external view returns (IPRBProxyPlugin plugin); + + /// @notice Retrieves the address of the plugin installed for the provided method selector. + /// @dev The zero address is returned if no plugin is installed. + /// @param proxy The proxy for the query. + /// @param method The method selector for the query. + function getPluginByProxy(IPRBProxy proxy, bytes4 method) external view returns (IPRBProxyPlugin plugin); + + /// @notice Retrieves the proxy for the provided user. + /// @param user The user address for the query. + function getProxy(address user) external view returns (IPRBProxy proxy); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Deploys a new proxy for the caller. + /// + /// @dev Emits a {DeployProxy} event. + /// + /// Requirements: + /// - The caller must not have a proxy. + /// + /// @return proxy The address of the newly deployed proxy. + function deploy() external returns (IPRBProxy proxy); + + /// @notice This function performs two actions: + /// 1. Deploys a new proxy for the caller + /// 2. Delegate calls to the provided target, returning the data it gets back, and bubbling up any potential revert. + /// + /// @dev Emits a {DeployProxy} and {Execute} event. + /// + /// Requirements: + /// - The caller must not have a proxy. + /// - `target` must be a contract. + /// + /// @param target The address of the target. + /// @param data Function selector plus ABI-encoded data. + /// @return proxy The address of the newly deployed proxy. + function deployAndExecute(address target, bytes calldata data) external returns (IPRBProxy proxy); + + /// @notice This function performs three actions: + /// 1. Deploys a new proxy for the caller + /// 2. Delegate calls to the provided target, returning the data it gets back, and bubbling up any potential revert. + /// 3. Installs the provided plugin on the newly deployed proxy. + /// + /// @dev Emits a {DeployProxy}, {Execute}, and {InstallPlugin} event. + /// + /// Requirements: + /// - The caller must not have a proxy. + /// - See the requirements in `installPlugin`. + /// - See the requirements in `execute`. + /// + /// @param target The address of the target. + /// @param data Function selector plus ABI-encoded data. + /// @param plugin The address of the plugin to install. + /// @return proxy The address of the newly deployed proxy. + function deployAndExecuteAndInstallPlugin( + address target, + bytes calldata data, + IPRBProxyPlugin plugin + ) + external + returns (IPRBProxy proxy); + + /// @notice This function performs two actions: + /// 1. Deploys a new proxy for the caller. + /// 2. Installs the provided plugin on the newly deployed proxy. + /// + /// @dev Emits a {DeployProxy} and {InstallPlugin} event. + /// + /// Requirements: + /// - The caller must not have a proxy. + /// - See the requirements in `installPlugin`. + /// + /// @param plugin The address of the plugin to install. + /// @return proxy The address of the newly deployed proxy. + function deployAndInstallPlugin(IPRBProxyPlugin plugin) external returns (IPRBProxy proxy); + + /// @notice Deploys a new proxy for the provided user. + /// + /// @dev Emits a {DeployProxy} event. + /// + /// Requirements: + /// - The user must not have a proxy already. + /// + /// @param user The address that will own the proxy. + /// @return proxy The address of the newly deployed proxy. + function deployFor(address user) external returns (IPRBProxy proxy); + + /// @notice Installs the provided plugin on the caller's proxy, and saves the list of methods implemented by the + /// plugin so that they can be referenced later. + /// + /// @dev Emits an {InstallPlugin} event. + /// + /// Notes: + /// - Installing a plugin is a potentially dangerous operation, because anyone can run the plugin. + /// - Plugin methods that have the same selectors as {IPRBProxy.execute}, {IPRBProxy.owner}, and + /// {IPRBProxy.registry} can be installed, but they can never be run. + /// + /// Requirements: + /// - The caller must have a proxy. + /// - The plugin must have at least one implemented method. + /// - There must be no method collision with any other plugin installed by the caller. + /// + /// @param plugin The address of the plugin to install. + function installPlugin(IPRBProxyPlugin plugin) external; + + /// @notice Gives or takes a permission from an envoy to call the provided target and function selector + /// on behalf of the caller's proxy. + /// + /// @dev Emits a {SetPermission} event. + /// + /// Notes: + /// - It is not an error to set the same permission. + /// + /// Requirements: + /// - The caller must have a proxy. + /// + /// @param envoy The address of the account the caller is giving or taking permission from. + /// @param target The address of the target. + /// @param permission The boolean permission to set. + function setPermission(address envoy, address target, bool permission) external; + + /// @notice Uninstalls the plugin from the caller's proxy, and removes the list of methods originally implemented by + /// the plugin. + /// + /// @dev Emits an {UninstallPlugin} event. + /// + /// Requirements: + /// - The caller must have a proxy. + /// - The plugin must be a known, previously installed plugin. + /// + /// @param plugin The address of the plugin to uninstall. + function uninstallPlugin(IPRBProxyPlugin plugin) external; +} diff --git a/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol index 7d68e0ac8..7b2dc87aa 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol @@ -16,18 +16,10 @@ import { TargetCheckout } from "./TargetCheckout.sol"; contract PluginCheckout is IPRBProxyPlugin, TargetCheckout { function getMethods() external pure override returns (bytes4[] memory) { - bytes4[] memory methods = new bytes4[](11); + bytes4[] memory methods = new bytes4[](3); methods[0] = this.withdraw.selector; - methods[1] = this.hasRole.selector; - methods[2] = this.getRoleAdmin.selector; - methods[3] = this.grantRole.selector; - methods[4] = this.revokeRole.selector; - methods[5] = this.renounceRole.selector; - methods[6] = this.DEFAULT_ADMIN_ROLE.selector; - methods[7] = this.getRoleMember.selector; - methods[8] = this.getRoleMemberCount.selector; - methods[9] = this.execute.selector; - methods[10] = this.swapAndExecute.selector; + methods[1] = this.execute.selector; + methods[2] = this.swapAndExecute.selector; return methods; } } diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol index 9e799fc3b..f18a94d70 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -7,8 +7,7 @@ import "../interface/IVault.sol"; import "../../../../lib/CurrencyTransferLib.sol"; import "../../../../eip/interface/IERC20.sol"; -import "../../../../extension/PermissionsEnumerable.sol"; -import "../../../../extension/Initializable.sol"; +import { IPRBProxy } from "./IPRBProxy.sol"; // $$\ $$\ $$\ $$\ $$\ // $$ | $$ | \__| $$ | $$ | @@ -19,17 +18,13 @@ import "../../../../extension/Initializable.sol"; // \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | // \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ -contract TargetCheckout is Initializable, PermissionsEnumerable, IExecutor { - function initialize(address _defaultAdmin) external initializer { - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - } - +contract TargetCheckout is IExecutor { // ================================================= // =============== Withdraw ======================== // ================================================= function withdraw(address _token, uint256 _amount) external { - require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized"); + require(msg.sender == IPRBProxy(address(this)).owner(), "Not authorized"); CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); } @@ -39,8 +34,6 @@ contract TargetCheckout is Initializable, PermissionsEnumerable, IExecutor { // ================================================= function execute(UserOp calldata op) external { - require(_canExecute(), "Not authorized"); - bool success; if (op.currency == CurrencyTransferLib.NATIVE_TOKEN) { (success, ) = op.target.call{ value: op.valueToSend }(op.data); @@ -56,12 +49,6 @@ contract TargetCheckout is Initializable, PermissionsEnumerable, IExecutor { } function swapAndExecute(UserOp calldata op, SwapOp calldata swap) external { - // require(_canExecute(), "Not authorized"); // TODO: Perform swap and execute here } - - // TODO: Re-evaluate utility of this function - function _canExecute() internal view returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, msg.sender); - } } From 0e623b12dee83f6165b26ef1b3dedbdd6dd99ac9 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 9 Jan 2024 21:14:22 +0530 Subject: [PATCH 15/20] add swap operation --- .../unaudited/checkout/prb/TargetCheckout.sol | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol index f18a94d70..9ef808c11 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -19,9 +19,7 @@ import { IPRBProxy } from "./IPRBProxy.sol"; // \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ contract TargetCheckout is IExecutor { - // ================================================= - // =============== Withdraw ======================== - // ================================================= + mapping(address => bool) public isApprovedRouter; function withdraw(address _token, uint256 _amount) external { require(msg.sender == IPRBProxy(address(this)).owner(), "Not authorized"); @@ -29,11 +27,29 @@ contract TargetCheckout is IExecutor { CurrencyTransferLib.transferCurrency(_token, address(this), msg.sender, _amount); } + function approveSwapRouter(address _swapRouter, bool _toApprove) external { + require(msg.sender == IPRBProxy(address(this)).owner(), "Not authorized"); + require(_swapRouter != address(0), "Zero address"); + + isApprovedRouter[_swapRouter] = _toApprove; + } + + function execute(UserOp calldata op) external { + _execute(op); + } + + function swapAndExecute(UserOp calldata op, SwapOp calldata swapOp) external { + require(isApprovedRouter[swapOp.router], "Invalid router address"); + + _swap(swapOp); + _execute(op); + } + // ================================================= - // =============== Executor functions ============== + // =============== Internal functions ============== // ================================================= - function execute(UserOp calldata op) external { + function _execute(UserOp calldata op) internal { bool success; if (op.currency == CurrencyTransferLib.NATIVE_TOKEN) { (success, ) = op.target.call{ value: op.valueToSend }(op.data); @@ -48,7 +64,28 @@ contract TargetCheckout is IExecutor { require(success, "Execution failed"); } - function swapAndExecute(UserOp calldata op, SwapOp calldata swap) external { - // TODO: Perform swap and execute here + function _swap(SwapOp memory _swapOp) internal { + address _tokenIn = _swapOp.tokenIn; + address _router = _swapOp.router; + + // get quote for amountIn + (, bytes memory quoteData) = _router.staticcall(_swapOp.quoteCalldata); + uint256 amountIn; + uint256 offset = _swapOp.amountInOffset; + + assembly { + amountIn := mload(add(add(quoteData, 32), offset)) + } + + // perform swap + bool success; + if (_tokenIn == CurrencyTransferLib.NATIVE_TOKEN) { + (success, ) = _router.call{ value: amountIn }(_swapOp.swapCalldata); + } else { + IERC20(_tokenIn).approve(_swapOp.router, amountIn); + (success, ) = _router.call(_swapOp.swapCalldata); + } + + require(success, "Swap failed"); } } From 7bb039be7ab2ea2fe32155ee166ddc6813478070 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 9 Jan 2024 21:31:46 +0530 Subject: [PATCH 16/20] update permission check --- .../unaudited/checkout/prb/TargetCheckout.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol index 9ef808c11..7cf40ccfd 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -35,12 +35,31 @@ contract TargetCheckout is IExecutor { } function execute(UserOp calldata op) external { + address owner = IPRBProxy(address(this)).owner(); + if (owner != msg.sender) { + bool permission = IPRBProxy(address(this)).registry().getPermissionByOwner({ + owner: owner, + envoy: msg.sender, + target: op.target + }); + require(permission, "Not authorized"); + } _execute(op); } function swapAndExecute(UserOp calldata op, SwapOp calldata swapOp) external { require(isApprovedRouter[swapOp.router], "Invalid router address"); + address owner = IPRBProxy(address(this)).owner(); + if (owner != msg.sender) { + bool permission = IPRBProxy(address(this)).registry().getPermissionByOwner({ + owner: owner, + envoy: msg.sender, + target: op.target + }); + require(permission, "Not authorized"); + } + _swap(swapOp); _execute(op); } From c41601adfe2b055226cb2ec27818ec08be291d80 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 9 Jan 2024 21:36:12 +0530 Subject: [PATCH 17/20] update permissions --- .../unaudited/checkout/prb/TargetCheckout.sol | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol index 7cf40ccfd..a4b590cc4 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -35,30 +35,14 @@ contract TargetCheckout is IExecutor { } function execute(UserOp calldata op) external { - address owner = IPRBProxy(address(this)).owner(); - if (owner != msg.sender) { - bool permission = IPRBProxy(address(this)).registry().getPermissionByOwner({ - owner: owner, - envoy: msg.sender, - target: op.target - }); - require(permission, "Not authorized"); - } + require(_canExecute(), "Not authorized"); + _execute(op); } function swapAndExecute(UserOp calldata op, SwapOp calldata swapOp) external { require(isApprovedRouter[swapOp.router], "Invalid router address"); - - address owner = IPRBProxy(address(this)).owner(); - if (owner != msg.sender) { - bool permission = IPRBProxy(address(this)).registry().getPermissionByOwner({ - owner: owner, - envoy: msg.sender, - target: op.target - }); - require(permission, "Not authorized"); - } + require(_canExecute(), "Not authorized"); _swap(swapOp); _execute(op); @@ -107,4 +91,19 @@ contract TargetCheckout is IExecutor { require(success, "Swap failed"); } + + function _canExecute() internal view returns (bool) { + address owner = IPRBProxy(address(this)).owner(); + if (owner != msg.sender) { + bool permission = IPRBProxy(address(this)).registry().getPermissionByOwner({ + owner: owner, + envoy: msg.sender, + target: op.target + }); + + return permission; + } + + return true; + } } From 5af284dcfce5d4eeb69ee9e53dcceec3526c814b Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 9 Jan 2024 21:40:35 +0530 Subject: [PATCH 18/20] fix --- .../unaudited/checkout/prb/TargetCheckout.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol index a4b590cc4..e821d11ce 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -35,14 +35,14 @@ contract TargetCheckout is IExecutor { } function execute(UserOp calldata op) external { - require(_canExecute(), "Not authorized"); + require(_canExecute(op, msg.sender), "Not authorized"); _execute(op); } function swapAndExecute(UserOp calldata op, SwapOp calldata swapOp) external { require(isApprovedRouter[swapOp.router], "Invalid router address"); - require(_canExecute(), "Not authorized"); + require(_canExecute(op, msg.sender), "Not authorized"); _swap(swapOp); _execute(op); @@ -92,12 +92,12 @@ contract TargetCheckout is IExecutor { require(success, "Swap failed"); } - function _canExecute() internal view returns (bool) { + function _canExecute(UserOp calldata op, address caller) internal view returns (bool) { address owner = IPRBProxy(address(this)).owner(); - if (owner != msg.sender) { + if (owner != caller) { bool permission = IPRBProxy(address(this)).registry().getPermissionByOwner({ owner: owner, - envoy: msg.sender, + envoy: caller, target: op.target }); From 3c37ec4ad152ab00cb8c92d808b89f02a3a7795e Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 9 Jan 2024 23:06:41 +0530 Subject: [PATCH 19/20] use prb-proxy package --- .gitmodules | 3 + .../unaudited/checkout/prb/IPRBProxy.sol | 66 ----- .../checkout/prb/IPRBProxyPlugin.sol | 19 -- .../checkout/prb/IPRBProxyRegistry.sol | 267 ------------------ .../unaudited/checkout/prb/PluginCheckout.sol | 2 +- .../unaudited/checkout/prb/TargetCheckout.sol | 2 +- foundry.toml | 1 + lib/prb-proxy | 1 + 8 files changed, 7 insertions(+), 354 deletions(-) delete mode 100644 contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol delete mode 100644 contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol delete mode 100644 contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol create mode 160000 lib/prb-proxy diff --git a/.gitmodules b/.gitmodules index 7872f5e50..0f105f378 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "lib/dynamic-contracts"] path = lib/dynamic-contracts url = https://github.com/thirdweb-dev/dynamic-contracts +[submodule "lib/prb-proxy"] + path = lib/prb-proxy + url = https://github.com/PaulRBerg/prb-proxy diff --git a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol deleted file mode 100644 index 4e4e83b87..000000000 --- a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxy.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.4; - -import { IPRBProxyPlugin } from "./IPRBProxyPlugin.sol"; -import { IPRBProxyRegistry } from "./IPRBProxyRegistry.sol"; - -/// @title IPRBProxy -/// @notice Proxy contract to compose transactions on behalf of the owner. -interface IPRBProxy { - /*////////////////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Thrown when a target contract reverts without a specified reason. - error PRBProxy_ExecutionReverted(); - - /// @notice Thrown when an unauthorized account tries to execute a delegate call. - error PRBProxy_ExecutionUnauthorized(address owner, address caller, address target); - - /// @notice Thrown when the fallback function fails to find an installed plugin for the method selector. - error PRBProxy_PluginNotInstalledForMethod(address caller, address owner, bytes4 method); - - /// @notice Thrown when a plugin execution reverts without a specified reason. - error PRBProxy_PluginReverted(IPRBProxyPlugin plugin); - - /// @notice Thrown when a non-contract address is passed as the target. - error PRBProxy_TargetNotContract(address target); - - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when a target contract is delegate called. - event Execute(address indexed target, bytes data, bytes response); - - /// @notice Emitted when a plugin is run for a provided method. - event RunPlugin(IPRBProxyPlugin indexed plugin, bytes data, bytes response); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice The address of the owner account or contract, which controls the proxy. - function owner() external view returns (address); - - /// @notice The address of the registry that has deployed this proxy. - function registry() external view returns (IPRBProxyRegistry); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Delegate calls to the provided target contract by forwarding the data. It returns the data it - /// gets back, and bubbles up any potential revert. - /// - /// @dev Emits an {Execute} event. - /// - /// Requirements: - /// - The caller must be either the owner or an envoy with permission. - /// - `target` must be a contract. - /// - /// @param target The address of the target contract. - /// @param data Function selector plus ABI encoded data. - /// @return response The response received from the target contract, if any. - function execute(address target, bytes calldata data) external payable returns (bytes memory response); -} diff --git a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol deleted file mode 100644 index fdafa4852..000000000 --- a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyPlugin.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.4; - -/// @title IPRBProxyPlugin -/// @notice Interface for plugin contracts that can be installed on a proxy. -/// @dev Plugins are contracts that enable the proxy to interact with and respond to calls from other contracts. These -/// plugins are run via the proxy's fallback function. -/// -/// This interface is meant to be directly inherited by plugin implementations. -interface IPRBProxyPlugin { - /// @notice Retrieves the methods implemented by the plugin. - /// @dev The registry pulls these methods when installing the plugin. - /// - /// Requirements: - /// - The plugin must implement at least one method. - /// - /// @return methods The array of the methods implemented by the plugin. - function getMethods() external returns (bytes4[] memory methods); -} diff --git a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol b/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol deleted file mode 100644 index cc6405751..000000000 --- a/contracts/prebuilts/unaudited/checkout/prb/IPRBProxyRegistry.sol +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.4; - -import { IPRBProxy } from "./IPRBProxy.sol"; -import { IPRBProxyPlugin } from "./IPRBProxyPlugin.sol"; - -/// @title IPRBProxyRegistry -/// @notice Deploys new proxies via CREATE2 and keeps a registry of owners to proxies. Proxies can only be deployed -/// once per owner, and they cannot be transferred. The registry also supports installing plugins, which are used -/// for extending the functionality of the proxy. -interface IPRBProxyRegistry { - /*////////////////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Thrown when trying to install a plugin that implements a method already implemented by another - /// installed plugin. - error PRBProxyRegistry_PluginMethodCollision( - IPRBProxyPlugin currentPlugin, IPRBProxyPlugin newPlugin, bytes4 method - ); - - /// @notice Thrown when trying to uninstall an unknown plugin. - error PRBProxyRegistry_PluginUnknown(IPRBProxyPlugin plugin); - - /// @notice Thrown when trying to install a plugin that doesn't implement any method. - error PRBProxyRegistry_PluginWithZeroMethods(IPRBProxyPlugin plugin); - - /// @notice Thrown when a function requires the user to have a proxy. - error PRBProxyRegistry_UserDoesNotHaveProxy(address user); - - /// @notice Thrown when a function requires the user to not have a proxy. - error PRBProxyRegistry_UserHasProxy(address user, IPRBProxy proxy); - - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when a new proxy is deployed. - event DeployProxy(address indexed operator, address indexed owner, IPRBProxy proxy); - - /// @notice Emitted when a plugin is installed. - event InstallPlugin( - address indexed owner, IPRBProxy indexed proxy, IPRBProxyPlugin indexed plugin, bytes4[] methods - ); - - /// @notice Emitted when an envoy's permission is updated. - event SetPermission( - address indexed owner, IPRBProxy indexed proxy, address indexed envoy, address target, bool newPermission - ); - - /// @notice Emitted when a plugin is uninstalled. - event UninstallPlugin( - address indexed owner, IPRBProxy indexed proxy, IPRBProxyPlugin indexed plugin, bytes4[] methods - ); - - /*////////////////////////////////////////////////////////////////////////// - STRUCTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @param owner The address of the user who will own the proxy. - /// @param target The address of the target to delegate call to. Can be set to zero. - /// @param data The call data to be passed to the target. Can be set to zero. - struct ConstructorParams { - address owner; - address target; - bytes data; - } - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice The release version of the proxy system, which applies to both the registry and deployed proxies. - /// @dev This is stored in the registry rather than the proxy to save gas for end users. - function VERSION() external view returns (string memory); - - /// @notice The parameters used in constructing the proxy, which the registry sets transiently during proxy - /// deployment. - /// @dev The proxy constructor fetches these parameters. - function constructorParams() external view returns (address owner, address target, bytes memory data); - - /// @notice Retrieves the list of installed methods for the provided plugin. - /// @dev An empty array is returned if the plugin is unknown. - /// @param owner The proxy owner for the query. - /// @param plugin The plugin for the query. - function getMethodsByOwner(address owner, IPRBProxyPlugin plugin) external view returns (bytes4[] memory methods); - - /// @notice Retrieves the list of installed methods for the provided plugin. - /// @dev An empty array is returned if the plugin is unknown. - /// @param proxy The proxy for the query. - /// @param plugin The plugin for the query. - function getMethodsByProxy( - IPRBProxy proxy, - IPRBProxyPlugin plugin - ) - external - view - returns (bytes4[] memory methods); - - /// @notice Retrieves a boolean flag that indicates whether the provided envoy has permission to call the provided - /// target. - /// @param owner The proxy owner for the query. - /// @param envoy The address checked for permission to call the target. - /// @param target The address of the target. - function getPermissionByOwner( - address owner, - address envoy, - address target - ) - external - view - returns (bool permission); - - /// @notice Retrieves a boolean flag that indicates whether the provided envoy has permission to call the provided - /// target. - /// @param proxy The proxy for the query. - /// @param envoy The address checked for permission to call the target. - /// @param target The address of the target. - function getPermissionByProxy( - IPRBProxy proxy, - address envoy, - address target - ) - external - view - returns (bool permission); - - /// @notice Retrieves the address of the plugin installed for the provided method selector. - /// @dev The zero address is returned if no plugin is installed. - /// @param owner The proxy owner for the query. - /// @param method The method selector for the query. - function getPluginByOwner(address owner, bytes4 method) external view returns (IPRBProxyPlugin plugin); - - /// @notice Retrieves the address of the plugin installed for the provided method selector. - /// @dev The zero address is returned if no plugin is installed. - /// @param proxy The proxy for the query. - /// @param method The method selector for the query. - function getPluginByProxy(IPRBProxy proxy, bytes4 method) external view returns (IPRBProxyPlugin plugin); - - /// @notice Retrieves the proxy for the provided user. - /// @param user The user address for the query. - function getProxy(address user) external view returns (IPRBProxy proxy); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Deploys a new proxy for the caller. - /// - /// @dev Emits a {DeployProxy} event. - /// - /// Requirements: - /// - The caller must not have a proxy. - /// - /// @return proxy The address of the newly deployed proxy. - function deploy() external returns (IPRBProxy proxy); - - /// @notice This function performs two actions: - /// 1. Deploys a new proxy for the caller - /// 2. Delegate calls to the provided target, returning the data it gets back, and bubbling up any potential revert. - /// - /// @dev Emits a {DeployProxy} and {Execute} event. - /// - /// Requirements: - /// - The caller must not have a proxy. - /// - `target` must be a contract. - /// - /// @param target The address of the target. - /// @param data Function selector plus ABI-encoded data. - /// @return proxy The address of the newly deployed proxy. - function deployAndExecute(address target, bytes calldata data) external returns (IPRBProxy proxy); - - /// @notice This function performs three actions: - /// 1. Deploys a new proxy for the caller - /// 2. Delegate calls to the provided target, returning the data it gets back, and bubbling up any potential revert. - /// 3. Installs the provided plugin on the newly deployed proxy. - /// - /// @dev Emits a {DeployProxy}, {Execute}, and {InstallPlugin} event. - /// - /// Requirements: - /// - The caller must not have a proxy. - /// - See the requirements in `installPlugin`. - /// - See the requirements in `execute`. - /// - /// @param target The address of the target. - /// @param data Function selector plus ABI-encoded data. - /// @param plugin The address of the plugin to install. - /// @return proxy The address of the newly deployed proxy. - function deployAndExecuteAndInstallPlugin( - address target, - bytes calldata data, - IPRBProxyPlugin plugin - ) - external - returns (IPRBProxy proxy); - - /// @notice This function performs two actions: - /// 1. Deploys a new proxy for the caller. - /// 2. Installs the provided plugin on the newly deployed proxy. - /// - /// @dev Emits a {DeployProxy} and {InstallPlugin} event. - /// - /// Requirements: - /// - The caller must not have a proxy. - /// - See the requirements in `installPlugin`. - /// - /// @param plugin The address of the plugin to install. - /// @return proxy The address of the newly deployed proxy. - function deployAndInstallPlugin(IPRBProxyPlugin plugin) external returns (IPRBProxy proxy); - - /// @notice Deploys a new proxy for the provided user. - /// - /// @dev Emits a {DeployProxy} event. - /// - /// Requirements: - /// - The user must not have a proxy already. - /// - /// @param user The address that will own the proxy. - /// @return proxy The address of the newly deployed proxy. - function deployFor(address user) external returns (IPRBProxy proxy); - - /// @notice Installs the provided plugin on the caller's proxy, and saves the list of methods implemented by the - /// plugin so that they can be referenced later. - /// - /// @dev Emits an {InstallPlugin} event. - /// - /// Notes: - /// - Installing a plugin is a potentially dangerous operation, because anyone can run the plugin. - /// - Plugin methods that have the same selectors as {IPRBProxy.execute}, {IPRBProxy.owner}, and - /// {IPRBProxy.registry} can be installed, but they can never be run. - /// - /// Requirements: - /// - The caller must have a proxy. - /// - The plugin must have at least one implemented method. - /// - There must be no method collision with any other plugin installed by the caller. - /// - /// @param plugin The address of the plugin to install. - function installPlugin(IPRBProxyPlugin plugin) external; - - /// @notice Gives or takes a permission from an envoy to call the provided target and function selector - /// on behalf of the caller's proxy. - /// - /// @dev Emits a {SetPermission} event. - /// - /// Notes: - /// - It is not an error to set the same permission. - /// - /// Requirements: - /// - The caller must have a proxy. - /// - /// @param envoy The address of the account the caller is giving or taking permission from. - /// @param target The address of the target. - /// @param permission The boolean permission to set. - function setPermission(address envoy, address target, bool permission) external; - - /// @notice Uninstalls the plugin from the caller's proxy, and removes the list of methods originally implemented by - /// the plugin. - /// - /// @dev Emits an {UninstallPlugin} event. - /// - /// Requirements: - /// - The caller must have a proxy. - /// - The plugin must be a known, previously installed plugin. - /// - /// @param plugin The address of the plugin to uninstall. - function uninstallPlugin(IPRBProxyPlugin plugin) external; -} diff --git a/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol index 7b2dc87aa..daccdd1cd 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.11; // \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | // \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ -import { IPRBProxyPlugin } from "./IPRBProxyPlugin.sol"; +import { IPRBProxyPlugin } from "@prb/proxy/src/interfaces/IPRBProxyPlugin.sol"; import { TargetCheckout } from "./TargetCheckout.sol"; diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol index e821d11ce..faf370c40 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -7,7 +7,7 @@ import "../interface/IVault.sol"; import "../../../../lib/CurrencyTransferLib.sol"; import "../../../../eip/interface/IERC20.sol"; -import { IPRBProxy } from "./IPRBProxy.sol"; +import { IPRBProxy } from "@prb/proxy/src/interfaces/IPRBProxy.sol"; // $$\ $$\ $$\ $$\ $$\ // $$ | $$ | \__| $$ | $$ | diff --git a/foundry.toml b/foundry.toml index a86fc9b13..2b603cf26 100644 --- a/foundry.toml +++ b/foundry.toml @@ -39,6 +39,7 @@ remappings = [ 'erc721a/=lib/ERC721A/', '@thirdweb-dev/dynamic-contracts/=lib/dynamic-contracts/', 'lib/sstore2=lib/dynamic-contracts/lib/sstore2/', + '@prb/proxy/=lib/prb-proxy/', ] fs_permissions = [{ access = "read-write", path = "./src/test/smart-wallet/utils"}] src = 'contracts' diff --git a/lib/prb-proxy b/lib/prb-proxy new file mode 160000 index 000000000..1c43be323 --- /dev/null +++ b/lib/prb-proxy @@ -0,0 +1 @@ +Subproject commit 1c43be323f8ff9f5105f9eb0b493ef5b7fc65c4b From 9d3b1133e05d1c830afd29fb62edf4135d579e70 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 10 Jan 2024 00:57:30 +0530 Subject: [PATCH 20/20] test PluginCheckout --- .../checkout/prb/IPluginCheckout.sol | 39 ++++++ .../unaudited/checkout/prb/PluginCheckout.sol | 2 +- .../unaudited/checkout/prb/TargetCheckout.sol | 6 +- src/test/checkout/PluginCheckout.t.sol | 123 ++++++++++++++++++ 4 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 contracts/prebuilts/unaudited/checkout/prb/IPluginCheckout.sol create mode 100644 src/test/checkout/PluginCheckout.t.sol diff --git a/contracts/prebuilts/unaudited/checkout/prb/IPluginCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/IPluginCheckout.sol new file mode 100644 index 000000000..9a174031e --- /dev/null +++ b/contracts/prebuilts/unaudited/checkout/prb/IPluginCheckout.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +interface IPluginCheckout { + /** + * @notice Details of the transaction to execute on target contract. + * + * @param target Address to send the transaction to + * + * @param currency Represents both native token and erc20 token + * + * @param approvalRequired If need to approve erc20 to the target contract + * + * @param valueToSend Transaction value to send - both native and erc20 + * + * @param data Transaction calldata + */ + struct UserOp { + address target; + address currency; + bool approvalRequired; + uint256 valueToSend; + bytes data; + } + + struct SwapOp { + address router; + address tokenOut; + address tokenIn; + uint256 amountIn; + uint256 amountInOffset; + bytes swapCalldata; + bytes quoteCalldata; + } + + function execute(UserOp calldata op) external; + + function swapAndExecute(UserOp calldata op, SwapOp memory swapOp) external; +} diff --git a/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol index daccdd1cd..d5d29796e 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol @@ -12,7 +12,7 @@ pragma solidity ^0.8.11; import { IPRBProxyPlugin } from "@prb/proxy/src/interfaces/IPRBProxyPlugin.sol"; -import { TargetCheckout } from "./TargetCheckout.sol"; +import "./TargetCheckout.sol"; contract PluginCheckout is IPRBProxyPlugin, TargetCheckout { function getMethods() external pure override returns (bytes4[] memory) { diff --git a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol index faf370c40..982f27705 100644 --- a/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol +++ b/contracts/prebuilts/unaudited/checkout/prb/TargetCheckout.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "../interface/IExecutor.sol"; -import "../interface/IVault.sol"; - import "../../../../lib/CurrencyTransferLib.sol"; import "../../../../eip/interface/IERC20.sol"; import { IPRBProxy } from "@prb/proxy/src/interfaces/IPRBProxy.sol"; +import "./IPluginCheckout.sol"; // $$\ $$\ $$\ $$\ $$\ // $$ | $$ | \__| $$ | $$ | @@ -18,7 +16,7 @@ import { IPRBProxy } from "@prb/proxy/src/interfaces/IPRBProxy.sol"; // \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | // \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ -contract TargetCheckout is IExecutor { +contract TargetCheckout is IPluginCheckout { mapping(address => bool) public isApprovedRouter; function withdraw(address _token, uint256 _amount) external { diff --git a/src/test/checkout/PluginCheckout.t.sol b/src/test/checkout/PluginCheckout.t.sol new file mode 100644 index 000000000..c116c4899 --- /dev/null +++ b/src/test/checkout/PluginCheckout.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../utils/BaseTest.sol"; + +import { IDrop } from "contracts/extension/interface/IDrop.sol"; + +import { PluginCheckout, IPluginCheckout } from "contracts/prebuilts/unaudited/checkout/prb/PluginCheckout.sol"; +import { IPRBProxyPlugin } from "@prb/proxy/src/interfaces/IPRBProxyPlugin.sol"; +import { IPRBProxy } from "@prb/proxy/src/interfaces/IPRBProxy.sol"; +import { IPRBProxyRegistry } from "@prb/proxy/src/interfaces/IPRBProxyRegistry.sol"; +import { PRBProxy } from "@prb/proxy/src/PRBProxy.sol"; +import { PRBProxyRegistry } from "@prb/proxy/src/PRBProxyRegistry.sol"; + +contract PluginCheckoutTest is BaseTest { + PluginCheckout internal checkoutPlugin; + PRBProxy internal proxy; + PRBProxyRegistry internal proxyRegistry; + + address internal owner; + address internal alice; + address internal bob; + address internal random; + + address internal receiver; + + DropERC721 internal targetDrop; + + MockERC20 internal mainCurrency; + MockERC20 internal altCurrencyOne; + MockERC20 internal altCurrencyTwo; + + function setClaimConditionCurrency(DropERC721 drop, address _currency) public { + DropERC721.ClaimCondition[] memory conditions = new DropERC721.ClaimCondition[](1); + conditions[0].maxClaimableSupply = type(uint256).max; + conditions[0].quantityLimitPerWallet = 100; + conditions[0].pricePerToken = 10; + conditions[0].currency = _currency; + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + } + + function setUp() public override { + super.setUp(); + + // setup actors + owner = getActor(1); + alice = getActor(2); + bob = getActor(3); + random = getActor(4); + receiver = getActor(5); + + // setup currencies + mainCurrency = new MockERC20(); + altCurrencyOne = new MockERC20(); + altCurrencyTwo = new MockERC20(); + + // mint and approve currencies + mainCurrency.mint(address(owner), 100 ether); + altCurrencyOne.mint(address(owner), 100 ether); + altCurrencyTwo.mint(address(owner), 100 ether); + + // setup target NFT Drop contract + targetDrop = DropERC721(getContract("DropERC721")); + vm.prank(deployer); + targetDrop.lazyMint(100, "ipfs://", ""); + setClaimConditionCurrency(targetDrop, address(mainCurrency)); + + // deploy contracts + checkoutPlugin = new PluginCheckout(); + proxyRegistry = new PRBProxyRegistry(); + + vm.prank(owner); + proxy = PRBProxy( + payable(address(proxyRegistry.deployAndInstallPlugin(IPRBProxyPlugin(address(checkoutPlugin))))) + ); + } + + function test_executor_executeOp() public { + // deposit currencies in vault + vm.startPrank(owner); + mainCurrency.transfer(address(proxy), 10 ether); + vm.stopPrank(); + + // create user op -- claim tokens on targetDrop + uint256 _quantityToClaim = 5; + uint256 _totalPrice = 5 * 10; // claim condition price is set as 10 above in setup + DropERC721.AllowlistProof memory alp; + bytes memory callData = abi.encodeWithSelector( + IDrop.claim.selector, + receiver, + _quantityToClaim, + address(mainCurrency), + 10, + alp, + "" + ); + IPluginCheckout.UserOp memory op = IPluginCheckout.UserOp({ + target: address(targetDrop), + currency: address(mainCurrency), + approvalRequired: true, + valueToSend: _totalPrice, + data: callData + }); + + // check state before + assertEq(targetDrop.balanceOf(receiver), 0); + assertEq(targetDrop.nextTokenIdToClaim(), 0); + assertEq(mainCurrency.balanceOf(address(proxy)), 10 ether); + assertEq(mainCurrency.balanceOf(address(saleRecipient)), 0); + + // execute + vm.prank(owner); + PluginCheckout(address(proxy)).execute(op); + + // check state after + assertEq(targetDrop.balanceOf(receiver), _quantityToClaim); + assertEq(targetDrop.nextTokenIdToClaim(), _quantityToClaim); + assertEq(mainCurrency.balanceOf(address(proxy)), 10 ether - _totalPrice); + assertEq(mainCurrency.balanceOf(address(saleRecipient)), _totalPrice - (_totalPrice * platformFeeBps) / 10_000); + } +}