diff --git a/contracts/stakeAndBake/StakeAndBake.sol b/contracts/stakeAndBake/StakeAndBake.sol index d96d76b..abd6655 100644 --- a/contracts/stakeAndBake/StakeAndBake.sol +++ b/contracts/stakeAndBake/StakeAndBake.sol @@ -23,6 +23,10 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { error IncorrectPermitAmount(); /// @dev error thrown when the remaining amount after taking a fee is zero error ZeroDepositAmount(); + /// @dev error thrown when an unauthorized account calls an operator only function + error UnauthorizedAccount(address account); + /// @dev error thrown when operator is changed to zero address + error ZeroAddress(); event DepositorAdded(address indexed vault, address indexed depositor); event DepositorRemoved(address indexed vault); @@ -30,12 +34,15 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { address indexed owner, StakeAndBakeData data ); + event FeeChanged(uint256 indexed oldFee, uint256 indexed newFee); + event OperatorRoleTransferred( + address indexed previousOperator, + address indexed newOperator + ); struct StakeAndBakeData { /// @notice vault Address of the vault we will deposit the minted LBTC to address vault; - /// @notice owner Address of the user staking and baking - address owner; /// @notice permitPayload Contents of permit approval signed by the user bytes permitPayload; /// @notice depositPayload Contains the parameters needed to complete a deposit @@ -50,6 +57,8 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { struct StakeAndBakeStorage { LBTC lbtc; mapping(address => IDepositor) depositors; + address operator; + uint256 fee; } // keccak256(abi.encode(uint256(keccak256("lombardfinance.storage.StakeAndBake")) - 1)) & ~bytes32(uint256(0xff)) @@ -62,14 +71,39 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { _disableInitializers(); } - function initialize(address lbtc_, address owner_) external initializer { - __Ownable_init(owner_); - __Ownable2Step_init(); + modifier onlyOperator() { + if (_getStakeAndBakeStorage().operator != _msgSender()) { + revert UnauthorizedAccount(_msgSender()); + } + _; + } + function initialize( + address lbtc_, + address owner_, + address operator_, + uint256 fee_ + ) external initializer { __ReentrancyGuard_init(); + __Ownable_init(owner_); + __Ownable2Step_init(); + StakeAndBakeStorage storage $ = _getStakeAndBakeStorage(); $.lbtc = LBTC(lbtc_); + $.fee = fee_; + $.operator = operator_; + } + + /** + * @notice Sets the claiming fee + * @param fee The fee to set + */ + function setFee(uint256 fee) external onlyOperator { + StakeAndBakeStorage storage $ = _getStakeAndBakeStorage(); + uint256 oldFee = $.fee; + $.fee = fee; + emit FeeChanged(oldFee, fee); } /** @@ -101,7 +135,11 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { function batchStakeAndBake(StakeAndBakeData[] calldata data) external { for (uint256 i; i < data.length; ) { try this.stakeAndBake(data[i]) {} catch { - emit BatchStakeAndBakeReverted(data[i].owner, data[i]); + Actions.DepositBtcAction memory action = Actions.depositBtc( + data[i].mintPayload[4:] + ); + address owner = action.recipient; + emit BatchStakeAndBakeReverted(owner, data[i]); } unchecked { @@ -138,11 +176,17 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { (uint256, uint256, uint8, bytes32, bytes32) ); + // Check the recipient. + Actions.DepositBtcAction memory action = Actions.depositBtc( + data.mintPayload[4:] + ); + address owner = action.recipient; + // We check if we can simply use transferFrom. // Otherwise, we permit the depositor to transfer the minted value. - if ($.lbtc.allowance(data.owner, address(this)) < permitAmount) + if ($.lbtc.allowance(owner, address(this)) < permitAmount) $.lbtc.permit( - data.owner, + owner, address(this), permitAmount, deadline, @@ -151,10 +195,10 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { s ); - $.lbtc.transferFrom(data.owner, address(this), permitAmount); + $.lbtc.transferFrom(owner, address(this), permitAmount); // Take the current maximum fee from the user. - uint256 feeAmount = $.lbtc.getMintFee(); + uint256 feeAmount = $.fee; $.lbtc.transfer($.lbtc.getTreasury(), feeAmount); uint256 remainingAmount = permitAmount - feeAmount; @@ -165,7 +209,7 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { $.lbtc.approve(address(depositor), remainingAmount); // Finally, deposit LBTC to the given `vault`. - depositor.deposit(data.vault, data.owner, data.depositPayload); + depositor.deposit(data.vault, owner, data.depositPayload); } function getStakeAndBakeFee() external view returns (uint256) { @@ -173,6 +217,16 @@ contract StakeAndBake is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { return $.lbtc.getMintFee(); } + function transferOperatorRole(address newOperator) external onlyOwner { + if (newOperator == address(0)) { + revert ZeroAddress(); + } + StakeAndBakeStorage storage $ = _getStakeAndBakeStorage(); + address oldOperator = $.operator; + $.operator = newOperator; + emit OperatorRoleTransferred(oldOperator, newOperator); + } + function _getStakeAndBakeStorage() private pure diff --git a/test/StakeAndBake.ts b/test/StakeAndBake.ts index 965a7b5..6899867 100644 --- a/test/StakeAndBake.ts +++ b/test/StakeAndBake.ts @@ -30,6 +30,7 @@ describe('StakeAndBake', function () { signer1: Signer, signer2: Signer, signer3: Signer, + operator: Signer, treasury: Signer; let stakeAndBake: StakeAndBake; let tellerWithMultiAssetSupportDepositor: TellerWithMultiAssetSupportDepositor; @@ -39,7 +40,7 @@ describe('StakeAndBake', function () { let snapshotTimestamp: number; before(async function () { - [deployer, signer1, signer2, signer3, treasury] = + [deployer, signer1, signer2, signer3, operator, treasury] = await getSignersWithPrivateKeys(); const burnCommission = 1000; @@ -53,6 +54,8 @@ describe('StakeAndBake', function () { stakeAndBake = await deployContract('StakeAndBake', [ await lbtc.getAddress(), deployer.address, + operator.address, + 1, ]); teller = await deployContract( @@ -122,9 +125,6 @@ describe('StakeAndBake', function () { encode(['uint256'], [0]) // txid ); - // set max fee - await lbtc.setMintFee(fee); - // create permit payload const block = await ethers.provider.getBlock('latest'); const timestamp = block!.timestamp; @@ -152,11 +152,62 @@ describe('StakeAndBake', function () { ); }); + it('should allow owner to change operator', async function () { + await expect(stakeAndBake.transferOperatorRole(signer2.address)) + .to.emit(stakeAndBake, 'OperatorRoleTransferred') + .withArgs(operator.address, signer2.address); + }); + + it('should not allow anyone else to change operator', async function () { + await expect( + stakeAndBake + .connect(signer2) + .transferOperatorRole(signer2.address) + ).to.be.reverted; + }); + + it('should allow operator to change the fee', async function () { + await expect(stakeAndBake.connect(operator).setFee(2)) + .to.emit(stakeAndBake, 'FeeChanged') + .withArgs(1, 2); + }); + + it('should not allow anyone else to change the fee', async function () { + await expect(stakeAndBake.setFee(2)).to.be.reverted; + }); + + it('should allow admin to add a depositor', async function () { + await expect( + stakeAndBake.addDepositor(signer1.address, signer2.address) + ) + .to.emit(stakeAndBake, 'DepositorAdded') + .withArgs(signer1.address, signer2.address); + }); + + it('should not allow anyone else to add a depositor', async function () { + await expect( + stakeAndBake + .connect(signer1) + .addDepositor(signer1.address, signer2.address) + ).to.be.reverted; + }); + + it('should allow admin to remove a depositor', async function () { + await expect(stakeAndBake.removeDepositor(signer1.address)) + .to.emit(stakeAndBake, 'DepositorRemoved') + .withArgs(signer1.address); + }); + + it('should not allow anyone else to remove a depositor', async function () { + await expect( + stakeAndBake.connect(signer1).removeDepositor(signer1.address) + ).to.be.reverted; + }); + it('should stake and bake properly with the correct setup', async function () { await expect( stakeAndBake.stakeAndBake({ vault: await teller.getAddress(), - owner: signer2.address, permitPayload: permitPayload, depositPayload: depositPayload, mintPayload: data.payload, @@ -198,6 +249,7 @@ describe('StakeAndBake', function () { depositValue - 50 ); }); + it('should work with allowance', async function () { await lbtc .connect(signer2) @@ -206,7 +258,6 @@ describe('StakeAndBake', function () { await expect( stakeAndBake.stakeAndBake({ vault: await teller.getAddress(), - owner: signer2.address, permitPayload: permitPayload, depositPayload: depositPayload, mintPayload: data.payload, @@ -248,6 +299,7 @@ describe('StakeAndBake', function () { depositValue - 50 ); }); + it('should batch stake and bake properly with the correct setup', async function () { // NB for some reason trying to do this in a loop and passing around arrays of parameters // makes the test fail, so i'm doing it the ugly way here @@ -292,7 +344,6 @@ describe('StakeAndBake', function () { stakeAndBake.batchStakeAndBake([ { vault: await teller.getAddress(), - owner: signer2.address, permitPayload: permitPayload, depositPayload: depositPayload, mintPayload: data.payload, @@ -300,7 +351,6 @@ describe('StakeAndBake', function () { }, { vault: await teller.getAddress(), - owner: signer3.address, permitPayload: permitPayload2, depositPayload: depositPayload2, mintPayload: data2.payload, @@ -377,6 +427,7 @@ describe('StakeAndBake', function () { depositValue - 50 ); }); + it('should revert when an unknown depositor is invoked', async function () { await expect( stakeAndBake.removeDepositor(await teller.getAddress()) @@ -387,7 +438,6 @@ describe('StakeAndBake', function () { await expect( stakeAndBake.stakeAndBake({ vault: await teller.getAddress(), - owner: signer2.address, permitPayload: permitPayload, depositPayload: depositPayload, mintPayload: data.payload, @@ -395,5 +445,18 @@ describe('StakeAndBake', function () { }) ).to.be.revertedWithCustomError(stakeAndBake, 'VaultNotFound'); }); + + it('should revert when remaining amount is zero', async function () { + await stakeAndBake.connect(operator).setFee(10001); + await expect( + stakeAndBake.stakeAndBake({ + vault: await teller.getAddress(), + permitPayload: permitPayload, + depositPayload: depositPayload, + mintPayload: data.payload, + proof: data.proof, + }) + ).to.be.revertedWithCustomError(stakeAndBake, 'ZeroDepositAmount'); + }); }); });