diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index 20c013a7..a9a59484 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -470,16 +470,18 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { PermitInput calldata permit ) external onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { from = (from == address(0)) ? msg.sender : from; - // solhint-disable-next-line func-named-parameters - _lido().permit( - from, - address(this), - permit.value, - permit.deadline, - permit.v, - permit.r, - permit.s - ); + if (_lido().allowance(from, address(this)) < permit.value) { + // solhint-disable-next-line func-named-parameters + _lido().permit( + from, + address(this), + permit.value, + permit.deadline, + permit.v, + permit.r, + permit.s + ); + } return _depositStETH(from, nodeOperatorId, stETHAmount); } @@ -520,16 +522,19 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 wstETHAmount, PermitInput calldata permit ) external onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { - // solhint-disable-next-line func-named-parameters - WSTETH.permit( - from, - address(this), - permit.value, - permit.deadline, - permit.v, - permit.r, - permit.s - ); + from = (from == address(0)) ? msg.sender : from; + if (WSTETH.allowance(from, address(this)) < permit.value) { + // solhint-disable-next-line func-named-parameters + WSTETH.permit( + from, + address(this), + permit.value, + permit.deadline, + permit.v, + permit.r, + permit.s + ); + } return _depositWstETH(from, nodeOperatorId, wstETHAmount); } diff --git a/src/interfaces/IStETH.sol b/src/interfaces/IStETH.sol index 1c098937..1d081802 100644 --- a/src/interfaces/IStETH.sol +++ b/src/interfaces/IStETH.sol @@ -78,4 +78,9 @@ interface IStETH { bytes32 r, bytes32 s ) external; + + function allowance( + address _owner, + address _spender + ) external view returns (uint256); } diff --git a/src/interfaces/IWstETH.sol b/src/interfaces/IWstETH.sol index 65ef5403..15e1a0ca 100644 --- a/src/interfaces/IWstETH.sol +++ b/src/interfaces/IWstETH.sol @@ -35,4 +35,9 @@ interface IWstETH { bytes32 r, bytes32 s ) external; + + function allowance( + address _owner, + address _spender + ) external view returns (uint256); } diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index 07f3bcde..0003d643 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -413,6 +413,49 @@ contract CSAccountingTest is ); } + function test_depositStETHWithPermit_alreadyPermitted() public { + _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); + vm.deal(user, 32 ether); + vm.prank(user); + stETH.submit{ value: 32 ether }({ _referal: address(0) }); + + vm.expectEmit(true, true, true, true, address(accounting)); + emit StETHBondDeposited(0, user, 32 ether); + + vm.mockCall( + address(stETH), + abi.encodeWithSelector( + stETH.permit.selector, + user, + address(accounting) + ), + abi.encode(32 ether) + ); + + vm.recordLogs(); + + vm.prank(stranger); + accounting.depositStETHWithPermit( + user, + 0, + 32 ether, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) + ); + + assertEq( + vm.getRecordedLogs().length, + 1, + "should emit only one event about deposit" + ); + } + function test_depositWstETHWithPermit() public { _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); vm.deal(user, 32 ether); @@ -461,6 +504,51 @@ contract CSAccountingTest is ); } + function test_depositWstETHWithPermit_alreadyPermitted() public { + _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); + vm.deal(user, 32 ether); + vm.startPrank(user); + stETH.submit{ value: 32 ether }({ _referal: address(0) }); + uint256 wstETHAmount = wstETH.wrap(32 ether); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(accounting)); + emit WstETHBondDeposited(0, user, wstETHAmount); + + vm.mockCall( + address(wstETH), + abi.encodeWithSelector( + wstETH.permit.selector, + user, + address(accounting) + ), + abi.encode(32 ether) + ); + + vm.recordLogs(); + + vm.prank(stranger); + accounting.depositWstETHWithPermit( + user, + 0, + wstETHAmount, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) + ); + + assertEq( + vm.getRecordedLogs().length, + 1, + "should emit only one event about deposit" + ); + } + function test_deposit_RevertIfNotExistedOperator() public { vm.expectRevert("node operator does not exist"); accounting.depositStETH(user, 0, 32 ether); diff --git a/test/helpers/Permit.sol b/test/helpers/Permit.sol index 0c3dbd3a..1655de80 100644 --- a/test/helpers/Permit.sol +++ b/test/helpers/Permit.sol @@ -24,6 +24,13 @@ contract PermitTokenBase { ) external { emit Approval(owner, spender, value); } + + function allowance( + address owner, + address spender + ) external view returns (uint256) { + return 0; + } } // https://eips.ethereum.org/EIPS/eip-2612