diff --git a/src/UniStaker.sol b/src/UniStaker.sol index f90b4c0..e9e76ee 100644 --- a/src/UniStaker.sol +++ b/src/UniStaker.sol @@ -13,6 +13,7 @@ contract UniStaker is ReentrancyGuard { error UniStaker__Unauthorized(bytes32 reason, address caller); error UniStaker__InvalidRewardRate(); error UniStaker__InsufficientRewardBalance(); + error UniStaker__InvalidAddress(); struct Deposit { uint256 balance; @@ -86,9 +87,51 @@ contract UniStaker is ReentrancyGuard { _depositId = _stake(_amount, _delegatee, _beneficiary); } + function stakeMore(DepositIdentifier _depositId, uint256 _amount) external nonReentrant { + Deposit storage deposit = deposits[_depositId]; + _revertIfNotDepositOwner(deposit); + + _updateReward(deposit.beneficiary); + + DelegationSurrogate _surrogate = surrogates[deposit.delegatee]; + _stakeTokenSafeTransferFrom(msg.sender, address(_surrogate), _amount); + + totalSupply += _amount; + totalDeposits[msg.sender] += _amount; + earningPower[deposit.beneficiary] += _amount; + deposit.balance += _amount; + } + + function alterDelegatee(DepositIdentifier _depositId, address _newDelegatee) public nonReentrant { + _revertIfAddressZero(_newDelegatee); + Deposit storage deposit = deposits[_depositId]; + _revertIfNotDepositOwner(deposit); + + DelegationSurrogate _oldSurrogate = surrogates[deposit.delegatee]; + deposit.delegatee = _newDelegatee; + DelegationSurrogate _newSurrogate = _fetchOrDeploySurrogate(_newDelegatee); + _stakeTokenSafeTransferFrom(address(_oldSurrogate), address(_newSurrogate), deposit.balance); + } + + function alterBeneficiary(DepositIdentifier _depositId, address _newBeneficiary) + public + nonReentrant + { + _revertIfAddressZero(_newBeneficiary); + Deposit storage deposit = deposits[_depositId]; + _revertIfNotDepositOwner(deposit); + + _updateReward(deposit.beneficiary); + earningPower[deposit.beneficiary] -= deposit.balance; + + _updateReward(_newBeneficiary); + deposit.beneficiary = _newBeneficiary; + earningPower[_newBeneficiary] += deposit.balance; + } + function withdraw(DepositIdentifier _depositId, uint256 _amount) external nonReentrant { Deposit storage deposit = deposits[_depositId]; - if (msg.sender != deposit.owner) revert UniStaker__Unauthorized("not owner", msg.sender); + _revertIfNotDepositOwner(deposit); _updateReward(deposit.beneficiary); @@ -148,6 +191,9 @@ contract UniStaker is ReentrancyGuard { internal returns (DepositIdentifier _depositId) { + _revertIfAddressZero(_delegatee); + _revertIfAddressZero(_beneficiary); + _updateReward(_beneficiary); DelegationSurrogate _surrogate = _fetchOrDeploySurrogate(_delegatee); @@ -176,4 +222,12 @@ contract UniStaker is ReentrancyGuard { rewards[_beneficiary] = earned(_beneficiary); userRewardPerTokenPaid[_beneficiary] = rewardPerTokenStored; } + + function _revertIfNotDepositOwner(Deposit storage deposit) internal view { + if (msg.sender != deposit.owner) revert UniStaker__Unauthorized("not owner", msg.sender); + } + + function _revertIfAddressZero(address _account) internal pure { + if (_account == address(0)) revert UniStaker__InvalidAddress(); + } } diff --git a/test/UniStaker.t.sol b/test/UniStaker.t.sol index 25fd00f..3f8fc03 100644 --- a/test/UniStaker.t.sol +++ b/test/UniStaker.t.sol @@ -43,10 +43,20 @@ contract UniStakerTest is Test { govToken.mint(_to, _amount); } + function _boundToRealisticStake(uint256 _stakeAmount) + public + view + returns (uint256 _boundedStakeAmount) + { + _boundedStakeAmount = bound(_stakeAmount, 0.1e18, 25_000_000e18); + } + function _stake(address _depositor, uint256 _amount, address _delegatee) internal returns (UniStaker.DepositIdentifier _depositId) { + vm.assume(_delegatee != address(0)); + vm.startPrank(_depositor); govToken.approve(address(uniStaker), _amount); _depositId = uniStaker.stake(_amount, _delegatee); @@ -57,6 +67,8 @@ contract UniStakerTest is Test { internal returns (UniStaker.DepositIdentifier _depositId) { + vm.assume(_delegatee != address(0) && _beneficiary != address(0)); + vm.startPrank(_depositor); govToken.approve(address(uniStaker), _amount); _depositId = uniStaker.stake(_amount, _delegatee, _beneficiary); @@ -466,6 +478,409 @@ contract Stake is UniStakerTest { _delegatee = address(uint160(uint256(keccak256(abi.encode(_delegatee))))); } } + + function testFuzz_RevertIf_DelegateeIsTheZeroAddress(address _depositor, uint256 _amount) public { + _amount = _boundMintAmount(_amount); + _mintGovToken(_depositor, _amount); + govToken.approve(address(uniStaker), _amount); + + vm.prank(_depositor); + vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector); + uniStaker.stake(_amount, address(0)); + } + + function testFuzz_RevertIf_BeneficiaryIsTheZeroAddress( + address _depositor, + uint256 _amount, + address _delegatee + ) public { + vm.assume(_delegatee != address(0)); + + _amount = _boundMintAmount(_amount); + _mintGovToken(_depositor, _amount); + govToken.approve(address(uniStaker), _amount); + + vm.prank(_depositor); + vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector); + uniStaker.stake(_amount, _delegatee, address(0)); + } +} + +contract StakeMore is UniStakerTest { + function testFuzz_TransfersStakeToTheExistingSurrogate( + address _depositor, + uint256 _depositAmount, + uint256 _addAmount, + address _delegatee, + address _beneficiary + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId); + DelegationSurrogate _surrogate = uniStaker.surrogates(_deposit.delegatee); + + _addAmount = _boundToRealisticStake(_addAmount); + _mintGovToken(_depositor, _addAmount); + + vm.startPrank(_depositor); + govToken.approve(address(uniStaker), _addAmount); + uniStaker.stakeMore(_depositId, _addAmount); + vm.stopPrank(); + + assertEq(govToken.balanceOf(address(_surrogate)), _depositAmount + _addAmount); + } + + function testFuzz_AddsToExistingBeneficiaryEarningPower( + address _depositor, + uint256 _depositAmount, + uint256 _addAmount, + address _delegatee, + address _beneficiary + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + + _addAmount = _boundToRealisticStake(_addAmount); + _mintGovToken(_depositor, _addAmount); + + vm.startPrank(_depositor); + govToken.approve(address(uniStaker), _addAmount); + uniStaker.stakeMore(_depositId, _addAmount); + vm.stopPrank(); + + assertEq(uniStaker.earningPower(_beneficiary), _depositAmount + _addAmount); + } + + function testFuzz_AddsToTheTotalSupply( + address _depositor, + uint256 _depositAmount, + uint256 _addAmount, + address _delegatee, + address _beneficiary + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + + _addAmount = _boundToRealisticStake(_addAmount); + _mintGovToken(_depositor, _addAmount); + + vm.startPrank(_depositor); + govToken.approve(address(uniStaker), _addAmount); + uniStaker.stakeMore(_depositId, _addAmount); + vm.stopPrank(); + + assertEq(uniStaker.totalSupply(), _depositAmount + _addAmount); + } + + function testFuzz_AddsToDepositorsTotalDeposits( + address _depositor, + uint256 _depositAmount, + uint256 _addAmount, + address _delegatee, + address _beneficiary + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + + _addAmount = _boundToRealisticStake(_addAmount); + _mintGovToken(_depositor, _addAmount); + + vm.startPrank(_depositor); + govToken.approve(address(uniStaker), _addAmount); + uniStaker.stakeMore(_depositId, _addAmount); + vm.stopPrank(); + + assertEq(uniStaker.totalDeposits(_depositor), _depositAmount + _addAmount); + } + + function testFuzz_AddsToTheDepositBalance( + address _depositor, + uint256 _depositAmount, + uint256 _addAmount, + address _delegatee, + address _beneficiary + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + + _addAmount = _boundToRealisticStake(_addAmount); + _mintGovToken(_depositor, _addAmount); + + vm.startPrank(_depositor); + govToken.approve(address(uniStaker), _addAmount); + uniStaker.stakeMore(_depositId, _addAmount); + vm.stopPrank(); + + UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId); + + assertEq(_deposit.balance, _depositAmount + _addAmount); + } + + function testFuzz_RevertIf_TheCallerIsNotTheDepositor( + address _depositor, + address _notDepositor, + uint256 _depositAmount, + uint256 _addAmount, + address _delegatee, + address _beneficiary + ) public { + vm.assume(_notDepositor != _depositor); + + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + + _addAmount = _boundToRealisticStake(_addAmount); + _mintGovToken(_depositor, _addAmount); + + vm.prank(_depositor); + govToken.approve(address(uniStaker), _addAmount); + + vm.prank(_notDepositor); + vm.expectRevert( + abi.encodeWithSelector( + UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor + ) + ); + uniStaker.stakeMore(_depositId, _addAmount); + } + + function testFuzz_RevertIf_TheDepositIdentifierIsInvalid( + address _depositor, + UniStaker.DepositIdentifier _depositId, + uint256 _addAmount + ) public { + vm.assume(_depositor != address(0)); + _addAmount = _boundToRealisticStake(_addAmount); + + // Since no deposits have been made yet, all DepositIdentifiers are invalid, and any call to + // add stake to one should revert. We rely on the default owner of any uninitialized deposit + // being address zero, which means the address attempting to alter it won't be able to. + vm.prank(_depositor); + vm.expectRevert( + abi.encodeWithSelector( + UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _depositor + ) + ); + uniStaker.stakeMore(_depositId, _addAmount); + } +} + +contract AlterDelegatee is UniStakerTest { + function testFuzz_AllowsStakerToUpdateTheirDelegatee( + address _depositor, + uint256 _depositAmount, + address _firstDelegatee, + address _beneficiary, + address _newDelegatee + ) public { + vm.assume(_newDelegatee != address(0) && _newDelegatee != _firstDelegatee); + + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _firstDelegatee, _beneficiary); + address _firstSurrogate = address(uniStaker.surrogates(_firstDelegatee)); + + vm.prank(_depositor); + uniStaker.alterDelegatee(_depositId, _newDelegatee); + + UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId); + address _newSurrogate = address(uniStaker.surrogates(_deposit.delegatee)); + + assertEq(_deposit.delegatee, _newDelegatee); + assertEq(govToken.balanceOf(_newSurrogate), _depositAmount); + assertEq(govToken.balanceOf(_firstSurrogate), 0); + } + + function testFuzz_AllowsStakerToReiterateTheirDelegatee( + address _depositor, + uint256 _depositAmount, + address _delegatee, + address _beneficiary + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + address _beforeSurrogate = address(uniStaker.surrogates(_delegatee)); + + // We are calling alterDelegatee with the address that is already the delegatee to ensure that + // doing so does not break anything other than wasting the user's gas + vm.prank(_depositor); + uniStaker.alterDelegatee(_depositId, _delegatee); + + UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId); + address _afterSurrogate = address(uniStaker.surrogates(_deposit.delegatee)); + + assertEq(_deposit.delegatee, _delegatee); + assertEq(_beforeSurrogate, _afterSurrogate); + assertEq(govToken.balanceOf(_afterSurrogate), _depositAmount); + } + + function testFuzz_RevertIf_TheCallerIsNotTheDepositor( + address _depositor, + address _notDepositor, + uint256 _depositAmount, + address _firstDelegatee, + address _beneficiary, + address _newDelegatee + ) public { + vm.assume( + _depositor != _notDepositor && _newDelegatee != address(0) && _newDelegatee != _firstDelegatee + ); + + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _firstDelegatee, _beneficiary); + + vm.prank(_notDepositor); + vm.expectRevert( + abi.encodeWithSelector( + UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor + ) + ); + uniStaker.alterDelegatee(_depositId, _newDelegatee); + } + + function testFuzz_RevertIf_TheDepositIdentifierIsInvalid( + address _depositor, + UniStaker.DepositIdentifier _depositId, + address _newDelegatee + ) public { + vm.assume(_depositor != address(0) && _newDelegatee != address(0)); + + // Since no deposits have been made yet, all DepositIdentifiers are invalid, and any call to + // alter one should revert. We rely on the default owner of any uninitialized deposit being + // address zero, which means the address attempting to alter it won't be able to. + vm.prank(_depositor); + vm.expectRevert( + abi.encodeWithSelector( + UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _depositor + ) + ); + uniStaker.alterDelegatee(_depositId, _newDelegatee); + } + + function testFuzz_RevertIf_DelegateeIsTheZeroAddress( + address _depositor, + uint256 _depositAmount, + address _delegatee + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee); + + vm.prank(_depositor); + vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector); + uniStaker.alterDelegatee(_depositId, address(0)); + } +} + +contract AlterBeneficiary is UniStakerTest { + function testFuzz_AllowsStakerToUpdateTheirBeneficiary( + address _depositor, + uint256 _depositAmount, + address _delegatee, + address _firstBeneficiary, + address _newBeneficiary + ) public { + vm.assume(_newBeneficiary != address(0) && _newBeneficiary != _firstBeneficiary); + + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _firstBeneficiary); + + vm.prank(_depositor); + uniStaker.alterBeneficiary(_depositId, _newBeneficiary); + + UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId); + + assertEq(_deposit.beneficiary, _newBeneficiary); + assertEq(uniStaker.earningPower(_newBeneficiary), _depositAmount); + assertEq(uniStaker.earningPower(_firstBeneficiary), 0); + } + + function testFuzz_AllowsStakerToReiterateTheirBeneficiary( + address _depositor, + uint256 _depositAmount, + address _delegatee, + address _beneficiary + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + + // We are calling alterBeneficiary with the address that is already the beneficiary to ensure + // that doing so does not break anything other than wasting the user's gas + vm.prank(_depositor); + uniStaker.alterBeneficiary(_depositId, _beneficiary); + + UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId); + + assertEq(_deposit.beneficiary, _beneficiary); + assertEq(uniStaker.earningPower(_beneficiary), _depositAmount); + } + + function testFuzz_RevertIf_TheCallerIsNotTheDepositor( + address _depositor, + address _notDepositor, + uint256 _depositAmount, + address _delegatee, + address _firstBeneficiary, + address _newBeneficiary + ) public { + vm.assume( + _notDepositor != _depositor && _newBeneficiary != address(0) + && _newBeneficiary != _firstBeneficiary + ); + + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _firstBeneficiary); + + vm.prank(_notDepositor); + vm.expectRevert( + abi.encodeWithSelector( + UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _notDepositor + ) + ); + uniStaker.alterBeneficiary(_depositId, _newBeneficiary); + } + + function testFuzz_RevertIf_TheDepositIdentifierIsInvalid( + address _depositor, + UniStaker.DepositIdentifier _depositId, + address _newBeneficiary + ) public { + vm.assume(_depositor != address(0) && _newBeneficiary != address(0)); + + // Since no deposits have been made yet, all DepositIdentifiers are invalid, and any call to + // alter one should revert. We rely on the default owner of any uninitialized deposit being + // address zero, which means the address attempting to alter it won't be able to. + vm.prank(_depositor); + vm.expectRevert( + abi.encodeWithSelector( + UniStaker.UniStaker__Unauthorized.selector, bytes32("not owner"), _depositor + ) + ); + uniStaker.alterBeneficiary(_depositId, _newBeneficiary); + } + + function testFuzz_RevertIf_BeneficiaryIsTheZeroAddress( + address _depositor, + uint256 _depositAmount, + address _delegatee + ) public { + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee); + + vm.prank(_depositor); + vm.expectRevert(UniStaker.UniStaker__InvalidAddress.selector); + uniStaker.alterBeneficiary(_depositId, address(0)); + } } contract Withdraw is UniStakerTest { @@ -843,7 +1258,7 @@ contract UniStakerRewardsTest is UniStakerTest { view returns (uint256 _boundedStakeAmount, uint256 _boundedRewardAmount) { - _boundedStakeAmount = bound(_stakeAmount, 0.1e18, 25_000_000e18); + _boundedStakeAmount = _boundToRealisticStake(_stakeAmount); _boundedRewardAmount = _boundToRealisticReward(_rewardAmount); } @@ -996,6 +1411,64 @@ contract Earned is UniStakerRewardsTest { assertLteWithinOnePercent(uniStaker.earned(_depositor), _rewardAmount); } + function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorAssignsABeneficiaryAndStakesForFullDuration( + address _depositor, + address _delegatee, + address _beneficiary, + uint256 _stakeAmount, + uint256 _rewardAmount + ) public { + (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); + + // A user deposits staking tokens w/ a beneficiary + _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary); + // The contract is notified of a reward + _mintTransferAndNotifyReward(_rewardAmount); + // The full duration passes + _jumpAheadByPercentOfRewardDuration(101); + + // The beneficiary should have earned all the rewards + assertLteWithinOnePercent(uniStaker.earned(_beneficiary), _rewardAmount); + } + + function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorUpdatesTheirBeneficiary( + address _depositor, + address _delegatee, + address _beneficiary1, + address _beneficiary2, + uint256 _stakeAmount, + uint256 _rewardAmount, + uint256 _percentDuration + ) public { + vm.assume( + _beneficiary1 != _beneficiary2 && _beneficiary1 != address(0) && _beneficiary2 != address(0) + ); + + (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); + _percentDuration = bound(_percentDuration, 0, 100); + + // A user deposits staking tokens w/ a beneficiary + (, UniStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary1); + // The contract is notified of a reward + _mintTransferAndNotifyReward(_rewardAmount); + // Part of the rewards duration passes + _jumpAheadByPercentOfRewardDuration(_percentDuration); + // The depositor alters their beneficiary + vm.prank(_depositor); + uniStaker.alterBeneficiary(_depositId, _beneficiary2); + // The rest of the duration elapses + _jumpAheadByPercentOfRewardDuration(100 - _percentDuration); + + // The beneficiary should have earned all the rewards + assertLteWithinOnePercent( + uniStaker.earned(_beneficiary1), _percentOf(_rewardAmount, _percentDuration) + ); + assertLteWithinOnePercent( + uniStaker.earned(_beneficiary2), _percentOf(_rewardAmount, 100 - _percentDuration) + ); + } + function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForPartialDuration( address _depositor, address _delegatee, @@ -1045,6 +1518,50 @@ contract Earned is UniStakerRewardsTest { assertLteWithinOnePercent(uniStaker.earned(_depositor2), _percentOf(_rewardAmount, 50)); } + function testFuzz_CalculatesCorrectEarningsForTwoUsersWhenOneStakesMorePartiallyThroughTheDuration( + address _depositor1, + address _depositor2, + address _delegatee, + uint256 _stakeAmount, + uint256 _rewardAmount + ) public { + vm.assume(_depositor1 != _depositor2); + (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); + + // A user deposits staking tokens + (, UniStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); + // Some time passes + _jumpAhead(3000); + // Another depositor deposits the same number of staking tokens + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + // The contract is notified of a reward + _mintTransferAndNotifyReward(_rewardAmount); + // One third of the duration passes + _jumpAheadByPercentOfRewardDuration(34); + // The first user triples their deposit by staking 2x more + _mintGovToken(_depositor1, 2 * _stakeAmount); + vm.startPrank(_depositor1); + govToken.approve(address(uniStaker), 2 * _stakeAmount); + uniStaker.stakeMore(_depositId1, 2 * _stakeAmount); + vm.stopPrank(); + // The rest of the duration passes + _jumpAheadByPercentOfRewardDuration(66); + + // Depositor 1 earns half the reward for one third the time and three quarters for two thirds of + // the time + uint256 _depositor1ExpectedEarnings = + _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 75), 66); + // Depositor 2 earns half the reward for one third the time and one quarter for two thirds of + // the time + uint256 _depositor2ExpectedEarnings = + _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 25), 66); + + // Each user should have earned half of the rewards + assertLteWithinOnePercent(uniStaker.earned(_depositor1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(uniStaker.earned(_depositor2), _depositor2ExpectedEarnings); + } + function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsPartiallyThroughTheDuration( address _depositor, address _delegatee,