diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 9a837ff5..4fdce46d 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -8,6 +8,7 @@ import {MulticallerWithSender} from "src/utils/MulticallerWithSender.sol"; import {BaseParameters} from "script/utils/parameters/BaseParameters.sol"; import {BaseSepoliaParameters} from "script/utils/parameters/BaseSepoliaParameters.sol"; +import {Pay} from "src/utils/Pay.sol"; // forge utils import {Script} from "lib/forge-std/src/Script.sol"; @@ -21,6 +22,7 @@ contract Setup is Script { address sUSDProxy, address pDAO, address zap, + address payable pay, address usdc, address weth ) public returns (Engine engine) { @@ -30,6 +32,7 @@ contract Setup is Script { _sUSDProxy: sUSDProxy, _pDAO: pDAO, _zap: zap, + _pay: pay, _usdc: usdc, _weth: weth }); @@ -56,6 +59,7 @@ contract DeployBase is Setup, BaseParameters { sUSDProxy: USD_PROXY_ANDROMEDA, pDAO: PDAO, zap: ZAP, + pay: PAY, usdc: USDC, weth: WETH }); @@ -78,6 +82,7 @@ contract DeployBaseSepolia is Setup, BaseSepoliaParameters { sUSDProxy: USD_PROXY_ANDROMEDA, pDAO: PDAO, zap: ZAP, + pay: PAY, usdc: USDC, weth: WETH }); @@ -99,3 +104,17 @@ contract DeployMulticallBase is Setup, BaseParameters { vm.stopBroadcast(); } } + +/// @dev steps to deploy and verify on Base: +/// (1) load the variables in the .env file via `source .env` +/// (2) run `forge script script/Deploy.s.sol:DeployPayBase --rpc-url $BASE_RPC_URL --etherscan-api-key $BASESCAN_API_KEY --broadcast --verify -vvvv` +contract DeployPayBase is Setup, BaseParameters { + function run() public { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privateKey); + + new Pay(WETH); + + vm.stopBroadcast(); + } +} diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index d1950723..4aec8eac 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -22,6 +22,7 @@ contract Setup is Script { address sUSDProxy, address pDAO, address zap, + address payable pay, address usdc, address weth ) public returns (Engine engine) { @@ -31,6 +32,7 @@ contract Setup is Script { _sUSDProxy: sUSDProxy, _pDAO: pDAO, _zap: zap, + _pay: pay, _usdc: usdc, _weth: weth }); @@ -51,6 +53,7 @@ contract DeployBase is Setup, BaseParameters { sUSDProxy: USD_PROXY_ANDROMEDA, pDAO: PDAO, zap: ZAP, + pay: PAY, usdc: USDC, weth: WETH }); @@ -73,6 +76,7 @@ contract DeployBaseSepolia is Setup, BaseSepoliaParameters { sUSDProxy: USD_PROXY_ANDROMEDA, pDAO: PDAO, zap: ZAP, + pay: PAY, usdc: USDC, weth: WETH }); diff --git a/script/utils/parameters/BaseParameters.sol b/script/utils/parameters/BaseParameters.sol index b4a5af16..3765615b 100644 --- a/script/utils/parameters/BaseParameters.sol +++ b/script/utils/parameters/BaseParameters.sol @@ -33,6 +33,9 @@ contract BaseParameters { address public constant ZAP = 0x84f531d85fAA7Be42f8a248B87e40f760e558F7C; + address payable public constant PAY = + payable(0x127Fb7602bF3De092d351f922791cF9a149A4837); + address public constant USDT = address(0); address public constant CBBTC = 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf; diff --git a/script/utils/parameters/BaseSepoliaParameters.sol b/script/utils/parameters/BaseSepoliaParameters.sol index 4fe1e5c9..1e50e11b 100644 --- a/script/utils/parameters/BaseSepoliaParameters.sol +++ b/script/utils/parameters/BaseSepoliaParameters.sol @@ -32,4 +32,6 @@ contract BaseSepoliaParameters { uint128 public constant SUSDC_SPOT_MARKET_ID = 1; address public constant ZAP = 0xC9aF789Ae606F69cF8Ed073A04eC92f2354b027d; + + address payable public constant PAY = payable(address(0)); } diff --git a/src/Engine.sol b/src/Engine.sol index da9a3a5c..b133dd27 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -14,6 +14,7 @@ import {IERC20} from "src/interfaces/tokens/IERC20.sol"; import {IWETH} from "src/interfaces/tokens/IWETH.sol"; import {MathLib} from "src/libraries/MathLib.sol"; import {MulticallablePayable} from "src/utils/MulticallablePayable.sol"; +import {Pay} from "src/utils/Pay.sol"; import {SignatureCheckerLib} from "src/libraries/SignatureCheckerLib.sol"; import {Zap} from "src/utils/zap/Zap.sol"; @@ -85,6 +86,9 @@ contract Engine is /// @notice Zap contract Zap internal immutable zap; + /// @notice Pay contract + Pay internal immutable pay; + IWETH public immutable WETH; IERC20 public immutable USDC; @@ -133,13 +137,14 @@ contract Engine is address _sUSDProxy, address _pDAO, address _zap, + address payable _pay, address _usdc, address _weth ) { if ( _perpsMarketProxy == address(0) || _spotMarketProxy == address(0) || _sUSDProxy == address(0) || _zap == address(0) - || _usdc == address(0) || _weth == address(0) + || _pay == address(0) || _usdc == address(0) || _weth == address(0) ) revert ZeroAddress(); PERPS_MARKET_PROXY = IPerpsMarketProxy(_perpsMarketProxy); @@ -147,6 +152,7 @@ contract Engine is SUSD = IERC20(_sUSDProxy); zap = Zap(_zap); + pay = Pay(_pay); USDC = IERC20(_usdc); WETH = IWETH(_weth); @@ -155,9 +161,6 @@ contract Engine is pDAO = _pDAO; } - /// @notice Allows the contract to receive ETH when unwrapping WETH - receive() external payable {} - /*////////////////////////////////////////////////////////////// UPGRADE MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -570,10 +573,8 @@ contract Engine is address(this) ); - // Convert WETH to ETH and send to user - WETH.withdraw(unwrappedWETH); - (bool result,) = msg.sender.call{value: unwrappedWETH}(""); - if (result != true) revert ETHTransferFailed(); + WETH.approve(address(pay), unwrappedWETH); + pay.unwrapAndPay(unwrappedWETH, msg.sender); } function _depositCollateral( diff --git a/src/interfaces/IEngine.sol b/src/interfaces/IEngine.sol index b96a3c5e..1323272e 100644 --- a/src/interfaces/IEngine.sol +++ b/src/interfaces/IEngine.sol @@ -128,9 +128,6 @@ interface IEngine { /// and msg.value is less than specified amount error InsufficientETHDeposit(uint256 sent, uint256 required); - /// @notice thrown when a call to transfer eth fails - error ETHTransferFailed(); - /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ diff --git a/src/utils/Pay.sol b/src/utils/Pay.sol new file mode 100644 index 00000000..76425d7d --- /dev/null +++ b/src/utils/Pay.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import {IWETH} from "src/interfaces/tokens/IWETH.sol"; + +/// @notice Pay contract for unwrapping WETH and sending it to a recipient +/// @author cmontecoding +contract Pay { + /// @notice WETH contract + IWETH public immutable WETH; + + /// @notice thrown when a call to transfer eth fails + error ETHTransferFailed(); + + constructor(address _weth) { + WETH = IWETH(_weth); + } + + /// @notice unwrap WETH and send it to a recipient + /// @param amount amount of WETH to unwrap + /// @param to recipient address + function unwrapAndPay(uint256 amount, address to) public { + WETH.transferFrom(msg.sender, address(this), amount); + WETH.withdraw(amount); + (bool success,) = to.call{value: amount}(""); + if (success != true) { + revert ETHTransferFailed(); + } + } + + receive() external payable {} +} diff --git a/test/Collateral.t.sol b/test/Collateral.t.sol index 08e1520d..70f0042c 100644 --- a/test/Collateral.t.sol +++ b/test/Collateral.t.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.27; import {IEngine} from "src/interfaces/IEngine.sol"; import {Bootstrap} from "test/utils/Bootstrap.sol"; +import {Pay} from "src/utils/Pay.sol"; +import {MaliciousReceiver} from "test/utils/MaliciousReceiver.sol"; contract CollateralTest is Bootstrap { function setUp() public { @@ -638,9 +640,7 @@ contract WithdrawCollateral is CollateralTest { _tolerance: SMALLER_AMOUNT }); - vm.expectRevert( - abi.encodeWithSelector(IEngine.ETHTransferFailed.selector) - ); + vm.expectRevert(); engine.withdrawCollateralETH({ _accountId: accountId, @@ -650,18 +650,3 @@ contract WithdrawCollateral is CollateralTest { vm.stopPrank(); } } - -// Helper contract that rejects ETH transfers -contract MaliciousReceiver { - receive() external payable { - revert("I reject ETH"); - } - - function onERC721Received(address, address, uint256, bytes calldata) - external - pure - returns (bytes4) - { - return 0x150b7a02; - } -} diff --git a/test/Deployment.t.sol b/test/Deployment.t.sol index e03a2cb3..1a7dd489 100644 --- a/test/Deployment.t.sol +++ b/test/Deployment.t.sol @@ -19,6 +19,7 @@ contract DeploymentTest is Test, Setup { address internal sUSDC = address(0x6); address internal zap = address(0x7); address internal weth = address(0x8); + address payable internal pay = payable(address(0x9)); /// keccak256(abi.encodePacked("Synthetic USD Coin Spot Market")) bytes32 internal constant _HASHED_SUSDC_NAME = @@ -42,6 +43,7 @@ contract DeploymentTest is Test, Setup { sUSDProxy: sUSDProxy, pDAO: pDAO, zap: zap, + pay: pay, usdc: usdc, weth: weth }); @@ -56,6 +58,7 @@ contract DeploymentTest is Test, Setup { sUSDProxy: sUSDProxy, pDAO: pDAO, zap: zap, + pay: pay, usdc: usdc, weth: weth }) {} catch (bytes memory reason) { @@ -70,6 +73,7 @@ contract DeploymentTest is Test, Setup { sUSDProxy: sUSDProxy, pDAO: pDAO, zap: zap, + pay: pay, usdc: usdc, weth: weth }) {} catch (bytes memory reason) { @@ -84,6 +88,7 @@ contract DeploymentTest is Test, Setup { sUSDProxy: address(0), pDAO: pDAO, zap: zap, + pay: pay, usdc: usdc, weth: weth }) {} catch (bytes memory reason) { @@ -98,6 +103,22 @@ contract DeploymentTest is Test, Setup { sUSDProxy: sUSDProxy, pDAO: pDAO, zap: address(0), + pay: pay, + usdc: usdc, + weth: weth + }) {} catch (bytes memory reason) { + assertEq(bytes4(reason), IEngine.ZeroAddress.selector); + } + } + + function test_deploy_pay_zero_address() public { + try setup.deploySystem({ + perpsMarketProxy: perpsMarketProxy, + spotMarketProxy: spotMarketProxy, + sUSDProxy: sUSDProxy, + pDAO: pDAO, + zap: zap, + pay: payable(address(0)), usdc: usdc, weth: weth }) {} catch (bytes memory reason) { @@ -112,6 +133,7 @@ contract DeploymentTest is Test, Setup { sUSDProxy: sUSDProxy, pDAO: pDAO, zap: zap, + pay: pay, usdc: address(0), weth: weth }) {} catch (bytes memory reason) { @@ -126,6 +148,7 @@ contract DeploymentTest is Test, Setup { sUSDProxy: sUSDProxy, pDAO: pDAO, zap: zap, + pay: pay, usdc: usdc, weth: address(0) }) {} catch (bytes memory reason) { diff --git a/test/EIP7412.t.sol b/test/EIP7412.t.sol index 00ec279e..aeb6e645 100644 --- a/test/EIP7412.t.sol +++ b/test/EIP7412.t.sol @@ -44,12 +44,14 @@ contract FulfillOracleQuery is EIP7412Test { { uint256 preBalance = address(this).balance; + // refunds are not supported + vm.expectRevert("EIP7412MockRefund"); + engine.fulfillOracleQuery{value: AMOUNT}( payable(address(eip7412MockRefund)), signedOffchainData ); - assert(address(this).balance == preBalance - AMOUNT); - assert(address(engine).balance == AMOUNT); + assert(address(this).balance == preBalance); } function test_fulfillOracleQuery_revert(bytes calldata signedOffchainData) diff --git a/test/Pay.t.sol b/test/Pay.t.sol new file mode 100644 index 00000000..63d3742a --- /dev/null +++ b/test/Pay.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import {Bootstrap} from "test/utils/Bootstrap.sol"; +import {MaliciousReceiver} from "test/utils/MaliciousReceiver.sol"; +import {Pay} from "src/utils/Pay.sol"; +import {IWETH} from "src/interfaces/tokens/IWETH.sol"; + +contract PayTest is Bootstrap { + Pay payLocal; + Pay payFork; + IWETH wethWrapped; + + function setUp() public { + vm.rollFork(BASE_BLOCK_NUMBER); + initializeBase(); + + wethWrapped = IWETH(weth); + payLocal = new Pay(weth); + payFork = Pay(pay); + } + + function test_unwrapAndPay_local() public { + uint256 amount = 100; + address to = address(1); + uint256 balanceBefore = to.balance; + + // setup + vm.deal(address(this), amount); + wethWrapped.deposit{value: amount}(); + wethWrapped.approve(address(payLocal), amount); + + // unwrap + payLocal.unwrapAndPay(amount, to); + assertEq(to.balance, balanceBefore + amount); + } + + function test_unwrapAndPay_local_fuzz(uint128 amount) public { + address to = address(1); + uint256 balanceBefore = to.balance; + + // setup + vm.deal(address(this), amount); + wethWrapped.deposit{value: amount}(); + wethWrapped.approve(address(payLocal), amount); + + // unwrap + payLocal.unwrapAndPay(amount, to); + assertEq(to.balance, balanceBefore + amount); + } + + function test_unwrapAndPay_withMaliciousReceiver() public { + uint256 amount = 100; + MaliciousReceiver maliciousReceiver = new MaliciousReceiver(); + address to = address(maliciousReceiver); + + // setup + vm.deal(address(this), amount); + wethWrapped.deposit{value: amount}(); + wethWrapped.approve(address(payLocal), amount); + + // Expect the ETHTransferFailed error + vm.expectRevert(Pay.ETHTransferFailed.selector); + payLocal.unwrapAndPay(amount, to); + } + + function test_unwrapAndPay_fork() public { + uint256 amount = 100; + address to = address(1); + uint256 balanceBefore = to.balance; + + // setup + vm.deal(address(this), amount); + wethWrapped.deposit{value: amount}(); + wethWrapped.approve(address(payFork), amount); + + // unwrap + payFork.unwrapAndPay(amount, to); + assertEq(to.balance, balanceBefore + amount); + } + + function test_unwrapAndPay_fork_fuzz(uint128 amount) public { + address to = address(1); + uint256 balanceBefore = to.balance; + + // setup + vm.deal(address(this), amount); + wethWrapped.deposit{value: amount}(); + wethWrapped.approve(address(payFork), amount); + + // unwrap + payFork.unwrapAndPay(amount, to); + assertEq(to.balance, balanceBefore + amount); + } +} diff --git a/test/Upgrade.t.sol b/test/Upgrade.t.sol index d36bf8fd..630c376a 100644 --- a/test/Upgrade.t.sol +++ b/test/Upgrade.t.sol @@ -41,6 +41,7 @@ contract MockUpgrade is UpgradeTest { address(sUSD), address(pDAO), address(zap), + payable(address(pay)), address(USDC), address(WETH) ); @@ -158,6 +159,7 @@ contract RemoveUpgradability is UpgradeTest { address(sUSD), address(0), // set pDAO to zero address to effectively remove upgradability address(zap), + payable(address(pay)), address(USDC), address(WETH) ); diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 2377111b..a6f64431 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -63,6 +63,7 @@ contract Bootstrap is IERC20 public cbBTC; IERC20 public USDe; address public zap; + address payable public pay; address public usdc; address public weth; @@ -75,13 +76,14 @@ contract Bootstrap is function initializeBase() public { BootstrapBase bootstrap = new BootstrapBase(); ( - address payable _engineAddress, - address payable _engineExposedAddress, + address _engineAddress, + address _engineExposedAddress, address _perpsMarketProxyAddress, address _spotMarketProxyAddress, address _sUSDAddress, address _pDAOAddress, address _zapAddress, + address payable _payAddress, address _usdcAddress, address _wethAddress, address _usdtAddress, @@ -102,6 +104,7 @@ contract Bootstrap is synthMinter = new SynthMinter(_sUSDAddress, _spotMarketProxyAddress); pDAO = _pDAOAddress; zap = _zapAddress; + pay = _payAddress; usdc = _usdcAddress; weth = _wethAddress; @@ -128,8 +131,6 @@ contract BootstrapBase is Setup, BaseParameters { function init() public returns ( - address payable, - address payable, address, address, address, @@ -137,6 +138,9 @@ contract BootstrapBase is Setup, BaseParameters { address, address, address, + address payable, + address, + address, address, address, address @@ -148,6 +152,7 @@ contract BootstrapBase is Setup, BaseParameters { sUSDProxy: USD_PROXY_ANDROMEDA, pDAO: PDAO, zap: ZAP, + pay: PAY, usdc: USDC, weth: WETH }); @@ -158,18 +163,20 @@ contract BootstrapBase is Setup, BaseParameters { _sUSDProxy: USD_PROXY_ANDROMEDA, _pDAO: PDAO, _zap: ZAP, + _pay: PAY, _usdc: USDC, _weth: WETH }); return ( - payable(address(engine)), - payable(address(engineExposed)), + address(engine), + address(engineExposed), PERPS_MARKET_PROXY_ANDROMEDA, SPOT_MARKET_PROXY_ANDROMEDA, USD_PROXY_ANDROMEDA, PDAO, ZAP, + PAY, USDC, WETH, USDT, diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 4ad57837..e518fb71 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -4,10 +4,8 @@ pragma solidity 0.8.27; /// @title Contract for defining constants used in testing /// @author JaredBorders (jaredborders@pm.me) contract Constants { - // /// @dev Dec-10-2024 09:34:19 PM +UTC - // uint256 public constant BASE_BLOCK_NUMBER = 23_538_556; - // - uint256 public constant BASE_BLOCK_NUMBER = 23_579_166; + /// @dev Dec-13-2024 07:33:47 PM +UTC + uint256 public constant BASE_BLOCK_NUMBER = 23_664_540; address internal constant OWNER = address(0x01); @@ -60,8 +58,8 @@ contract Constants { uint128 constant CBBTC_SYNTH_MARKET_ID = 4; - /// @dev this is the ETH price in USD at the block number 23_538_556 - uint256 internal constant ETH_PRICE = 3630; + /// @dev this is the ETH price in USD at the block number 23_664_540 + uint256 internal constant ETH_PRICE = 3918; uint256 internal constant AMOUNT = 10_000 ether; diff --git a/test/utils/MaliciousReceiver.sol b/test/utils/MaliciousReceiver.sol new file mode 100644 index 00000000..08b840f5 --- /dev/null +++ b/test/utils/MaliciousReceiver.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/// @notice Helper contract that rejects ETH transfers +/// @author cmontecoding +contract MaliciousReceiver { + receive() external payable { + revert(); + } + + function onERC721Received(address, address, uint256, bytes calldata) + external + pure + returns (bytes4) + { + return 0x150b7a02; + } +} diff --git a/test/utils/exposed/EngineExposed.sol b/test/utils/exposed/EngineExposed.sol index 29c056d9..95974813 100644 --- a/test/utils/exposed/EngineExposed.sol +++ b/test/utils/exposed/EngineExposed.sol @@ -12,6 +12,7 @@ contract EngineExposed is Engine { address _sUSDProxy, address _pDAO, address _zap, + address payable _pay, address _usdc, address _weth ) @@ -21,6 +22,7 @@ contract EngineExposed is Engine { _sUSDProxy, _pDAO, _zap, + _pay, _usdc, _weth ) diff --git a/test/utils/mocks/MockEngineUpgrade.sol b/test/utils/mocks/MockEngineUpgrade.sol index ef6295fb..86efda93 100644 --- a/test/utils/mocks/MockEngineUpgrade.sol +++ b/test/utils/mocks/MockEngineUpgrade.sol @@ -12,6 +12,7 @@ contract MockEngineUpgrade is Engine { address _sUSDProxy, address _pDAO, address _zap, + address payable _pay, address _usdc, address _weth ) @@ -21,6 +22,7 @@ contract MockEngineUpgrade is Engine { _sUSDProxy, _pDAO, _zap, + _pay, _usdc, _weth )