From 072ae63a04c6cc7d1778a7c35fccfe5bcf27e026 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 17 Jun 2020 17:06:39 +0200 Subject: [PATCH] Extracted common bonding parts from KeepBonding Moved parts of the code that are common and will be used for ETH bonding from KeepBonding to AbstractBonding contract. Only withdraw related functions remain in KeepBonding as they require different roles handling. --- solidity/contracts/AbstractBonding.sol | 348 +++++++++++++++++++++++++ solidity/contracts/KeepBonding.sol | 323 +---------------------- 2 files changed, 353 insertions(+), 318 deletions(-) create mode 100644 solidity/contracts/AbstractBonding.sol diff --git a/solidity/contracts/AbstractBonding.sol b/solidity/contracts/AbstractBonding.sol new file mode 100644 index 000000000..897c899a4 --- /dev/null +++ b/solidity/contracts/AbstractBonding.sol @@ -0,0 +1,348 @@ +/** +▓▓▌ ▓▓ ▐▓▓ ▓▓▓▓▓▓▓▓▓▓▌▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▄ +▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▌▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + ▓▓▓▓▓▓ ▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓ ▐▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓ ▐▓▓▓▓▓▌ ▐▓▓▓▓▓▓ + ▓▓▓▓▓▓▄▄▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓▄▄▄▄ ▓▓▓▓▓▓▄▄▄▄ ▐▓▓▓▓▓▌ ▐▓▓▓▓▓▓ + ▓▓▓▓▓▓▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▌ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + ▓▓▓▓▓▓▀▀▓▓▓▓▓▓▄ ▐▓▓▓▓▓▓▀▀▀▀ ▓▓▓▓▓▓▀▀▀▀ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▀ + ▓▓▓▓▓▓ ▀▓▓▓▓▓▓▄ ▐▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓ ▐▓▓▓▓▓▌ +▓▓▓▓▓▓▓▓▓▓ █▓▓▓▓▓▓▓▓▓ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ + + Trust math, not hardware. +*/ + +pragma solidity 0.5.17; + +import "@keep-network/keep-core/contracts/KeepRegistry.sol"; +import "@keep-network/keep-core/contracts/KeepStaking.sol"; +import "@keep-network/sortition-pools/contracts/api/IBonding.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + +/// @title Keep Bonding +/// @notice Contract holding deposits from keeps' operators. +contract AbstractBonding is IBonding { + using SafeMath for uint256; + + // Registry contract with a list of approved factories (operator contracts). + KeepRegistry internal registry; + + // Staking contract. + KeepStaking internal staking; + + // Unassigned value in wei deposited by operators. + mapping(address => uint256) public unbondedValue; + + // References to created bonds. Bond identifier is built from operator's + // address, holder's address and reference ID assigned on bond creation. + mapping(bytes32 => uint256) internal lockedBonds; + + // Sortition pools authorized by operator's authorizer. + // operator -> pool -> boolean + mapping(address => mapping(address => bool)) internal authorizedPools; + + event UnbondedValueDeposited( + address indexed operator, + address indexed beneficiary, + uint256 amount + ); + event UnbondedValueWithdrawn( + address indexed operator, + address indexed beneficiary, + uint256 amount + ); + event BondCreated( + address indexed operator, + address indexed holder, + address indexed sortitionPool, + uint256 referenceID, + uint256 amount + ); + event BondReassigned( + address indexed operator, + uint256 indexed referenceID, + address newHolder, + uint256 newReferenceID + ); + event BondReleased(address indexed operator, uint256 indexed referenceID); + event BondSeized( + address indexed operator, + uint256 indexed referenceID, + address destination, + uint256 amount + ); + + /// @notice Initializes Keep Bonding contract. + /// @param registryAddress Keep registry contract address. + /// @param stakingContractAddress Keep staking contract address. + constructor(address registryAddress, address stakingContractAddress) + public + { + registry = KeepRegistry(registryAddress); + staking = KeepStaking(stakingContractAddress); // Rename to: Staking + } + + /// @notice Add the provided value to operator's pool available for bonding. + /// @param operator Address of the operator. + function deposit(address operator) external payable { + address beneficiary = tokenStaking.beneficiaryOf(operator); + // Beneficiary has to be set (delegation exist) before an operator can + // deposit wei. It protects from a situation when an operator wants + // to withdraw funds which are transfered to beneficiary with zero + // address. + require( + beneficiary != address(0), + "Beneficiary not defined for the operator" + ); + unbondedValue[operator] = unbondedValue[operator].add(msg.value); + emit UnbondedValueDeposited(operator, beneficiary, msg.value); + } + + /// @notice Withdraws amount from operator's value available for bonding. + /// @param amount Value to withdraw in wei. + /// @param operator Address of the operator. + function withdraw(uint256 amount, address operator) public; + + /// @notice Returns the amount of wei the operator has made available for + /// bonding and that is still unbounded. If the operator doesn't exist or + /// bond creator is not authorized as an operator contract or it is not + /// authorized by the operator or there is no secondary authorization for + /// the provided sortition pool, function returns 0. + /// @dev Implements function expected by sortition pools' IBonding interface. + /// @param operator Address of the operator. + /// @param bondCreator Address authorized to create a bond. + /// @param authorizedSortitionPool Address of authorized sortition pool. + /// @return Amount of authorized wei deposit available for bonding. + function availableUnbondedValue( + address operator, + address bondCreator, + address authorizedSortitionPool + ) public view returns (uint256) { + // Sortition pools check this condition and skips operators that + // are no longer eligible. We cannot revert here. + if ( + registry.isApprovedOperatorContract(bondCreator) && + staking.isAuthorizedForOperator(operator, bondCreator) && + hasSecondaryAuthorization(operator, authorizedSortitionPool) + ) { + return unbondedValue[operator]; + } + + return 0; + } + + /// @notice Create bond for the given operator, holder, reference and amount. + /// @dev Function can be executed only by authorized contract. Reference ID + /// should be unique for holder and operator. + /// @param operator Address of the operator to bond. + /// @param holder Address of the holder of the bond. + /// @param referenceID Reference ID used to track the bond by holder. + /// @param amount Value to bond in wei. + /// @param authorizedSortitionPool Address of authorized sortition pool. + function createBond( + address operator, + address holder, + uint256 referenceID, + uint256 amount, + address authorizedSortitionPool + ) public { + require( + availableUnbondedValue( + operator, + msg.sender, + authorizedSortitionPool + ) >= amount, + "Insufficient unbonded value" + ); + + bytes32 bondID = keccak256( + abi.encodePacked(operator, holder, referenceID) + ); + + require( + lockedBonds[bondID] == 0, + "Reference ID not unique for holder and operator" + ); + + unbondedValue[operator] = unbondedValue[operator].sub(amount); + lockedBonds[bondID] = lockedBonds[bondID].add(amount); + + emit BondCreated( + operator, + holder, + authorizedSortitionPool, + referenceID, + amount + ); + } + + /// @notice Returns value of wei bonded for the operator. + /// @param operator Address of the operator. + /// @param holder Address of the holder of the bond. + /// @param referenceID Reference ID of the bond. + /// @return Amount of wei in the selected bond. + function bondAmount( + address operator, + address holder, + uint256 referenceID + ) public view returns (uint256) { + bytes32 bondID = keccak256( + abi.encodePacked(operator, holder, referenceID) + ); + + return lockedBonds[bondID]; + } + + /// @notice Reassigns a bond to a new holder under a new reference. + /// @dev Function requires that a caller is the current holder of the bond + /// which is being reassigned. + /// @param operator Address of the bonded operator. + /// @param referenceID Reference ID of the bond. + /// @param newHolder Address of the new holder of the bond. + /// @param newReferenceID New reference ID to register the bond. + function reassignBond( + address operator, + uint256 referenceID, + address newHolder, + uint256 newReferenceID + ) public { + address holder = msg.sender; + bytes32 bondID = keccak256( + abi.encodePacked(operator, holder, referenceID) + ); + + require(lockedBonds[bondID] > 0, "Bond not found"); + + bytes32 newBondID = keccak256( + abi.encodePacked(operator, newHolder, newReferenceID) + ); + + require( + lockedBonds[newBondID] == 0, + "Reference ID not unique for holder and operator" + ); + + lockedBonds[newBondID] = lockedBonds[bondID]; + lockedBonds[bondID] = 0; + + emit BondReassigned(operator, referenceID, newHolder, newReferenceID); + } + + /// @notice Releases the bond and moves the bond value to the operator's + /// unbounded value pool. + /// @dev Function requires that caller is the holder of the bond which is + /// being released. + /// @param operator Address of the bonded operator. + /// @param referenceID Reference ID of the bond. + function freeBond(address operator, uint256 referenceID) public { + address holder = msg.sender; + bytes32 bondID = keccak256( + abi.encodePacked(operator, holder, referenceID) + ); + + require(lockedBonds[bondID] > 0, "Bond not found"); + + uint256 amount = lockedBonds[bondID]; + lockedBonds[bondID] = 0; + unbondedValue[operator] = unbondedValue[operator].add(amount); + + emit BondReleased(operator, referenceID); + } + + /// @notice Seizes the bond by moving some or all of the locked bond to the + /// provided destination address. + /// @dev Function requires that a caller is the holder of the bond which is + /// being seized. + /// @param operator Address of the bonded operator. + /// @param referenceID Reference ID of the bond. + /// @param amount Amount to be seized. + /// @param destination Address to send the amount to. + function seizeBond( + address operator, + uint256 referenceID, + uint256 amount, + address payable destination + ) public { + require(amount > 0, "Requested amount should be greater than zero"); + + address payable holder = msg.sender; + bytes32 bondID = keccak256( + abi.encodePacked(operator, holder, referenceID) + ); + + require( + lockedBonds[bondID] >= amount, + "Requested amount is greater than the bond" + ); + + lockedBonds[bondID] = lockedBonds[bondID].sub(amount); + + (bool success, ) = destination.call.value(amount)(""); + require(success, "Transfer failed"); + + emit BondSeized(operator, referenceID, destination, amount); + } + + /// @notice Authorizes sortition pool for the provided operator. + /// Operator's authorizers need to authorize individual sortition pools + /// per application since they may be interested in participating only in + /// a subset of keep types used by the given application. + /// @dev Only operator's authorizer can call this function. + function authorizeSortitionPoolContract( + address _operator, + address _poolAddress + ) public { + require( + staking.authorizerOf(_operator) == msg.sender, + "Not authorized" + ); + authorizedPools[_operator][_poolAddress] = true; + } + + /// @notice Deauthorizes sortition pool for the provided operator. + /// Authorizer may deauthorize individual sortition pool in case the + /// operator should no longer be eligible for work selection and the + /// application represented by the sortition pool should no longer be + /// eligible to create bonds for the operator. + /// @dev Only operator's authorizer can call this function. + function deauthorizeSortitionPoolContract( + address _operator, + address _poolAddress + ) public { + require( + staking.authorizerOf(_operator) == msg.sender, + "Not authorized" + ); + authorizedPools[_operator][_poolAddress] = false; + } + + /// @notice Checks if the sortition pool has been authorized for the + /// provided operator by its authorizer. + /// @dev See authorizeSortitionPoolContract. + function hasSecondaryAuthorization(address _operator, address _poolAddress) + public + view + returns (bool) + { + return authorizedPools[_operator][_poolAddress]; + } + + /// @notice Withdraws the provided amount from unbonded value of the + /// provided operator to operator's beneficiary. If there is no enough + /// unbonded value or the transfer failed, function fails. + function withdrawBond(uint256 amount, address operator) internal { + require( + unbondedValue[operator] >= amount, + "Insufficient unbonded value" + ); + + unbondedValue[operator] = unbondedValue[operator].sub(amount); + + address beneficiary = staking.beneficiaryOf(operator); + + (bool success, ) = beneficiary.call.value(amount)(""); + require(success, "Transfer failed"); + + emit UnbondedValueWithdrawn(operator, beneficiary, amount); + } +} diff --git a/solidity/contracts/KeepBonding.sol b/solidity/contracts/KeepBonding.sol index 6e45ae1ef..3fa57b578 100644 --- a/solidity/contracts/KeepBonding.sol +++ b/solidity/contracts/KeepBonding.sol @@ -14,71 +14,19 @@ pragma solidity 0.5.17; -import "@keep-network/keep-core/contracts/KeepRegistry.sol"; -import "@keep-network/keep-core/contracts/TokenStaking.sol"; +import "./AbstractBonding.sol"; + import "@keep-network/keep-core/contracts/TokenGrant.sol"; import "@keep-network/keep-core/contracts/libraries/RolesLookup.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - - /// @title Keep Bonding /// @notice Contract holding deposits from keeps' operators. -contract KeepBonding { - using SafeMath for uint256; +contract KeepBonding is AbstractBonding { using RolesLookup for address payable; - // Registry contract with a list of approved factories (operator contracts). - KeepRegistry internal registry; - - // KEEP token staking contract. - TokenStaking internal tokenStaking; - // KEEP token grant contract. TokenGrant internal tokenGrant; - // Unassigned value in wei deposited by operators. - mapping(address => uint256) public unbondedValue; - - // References to created bonds. Bond identifier is built from operator's - // address, holder's address and reference ID assigned on bond creation. - mapping(bytes32 => uint256) internal lockedBonds; - - // Sortition pools authorized by operator's authorizer. - // operator -> pool -> boolean - mapping(address => mapping(address => bool)) internal authorizedPools; - - event UnbondedValueDeposited( - address indexed operator, - address indexed beneficiary, - uint256 amount - ); - event UnbondedValueWithdrawn( - address indexed operator, - address indexed beneficiary, - uint256 amount - ); - event BondCreated( - address indexed operator, - address indexed holder, - address indexed sortitionPool, - uint256 referenceID, - uint256 amount - ); - event BondReassigned( - address indexed operator, - uint256 indexed referenceID, - address newHolder, - uint256 newReferenceID - ); - event BondReleased(address indexed operator, uint256 indexed referenceID); - event BondSeized( - address indexed operator, - uint256 indexed referenceID, - address destination, - uint256 amount - ); - /// @notice Initializes Keep Bonding contract. /// @param registryAddress Keep registry contract address. /// @param tokenStakingAddress KEEP token staking contract address. @@ -87,56 +35,10 @@ contract KeepBonding { address registryAddress, address tokenStakingAddress, address tokenGrantAddress - ) public { - registry = KeepRegistry(registryAddress); - tokenStaking = TokenStaking(tokenStakingAddress); + ) public AbstractBonding(registryAddress, tokenStakingAddress) { tokenGrant = TokenGrant(tokenGrantAddress); } - /// @notice Add the provided value to operator's pool available for bonding. - /// @param operator Address of the operator. - function deposit(address operator) external payable { - address beneficiary = tokenStaking.beneficiaryOf(operator); - // Beneficiary has to be set (delegation exist) before an operator can - // deposit wei. It protects from a situation when an operator wants - // to withdraw funds which are transfered to beneficiary with zero - // address. - require( - beneficiary != address(0), - "Beneficiary not defined for the operator" - ); - unbondedValue[operator] = unbondedValue[operator].add(msg.value); - emit UnbondedValueDeposited(operator, beneficiary, msg.value); - } - - /// @notice Returns the amount of wei the operator has made available for - /// bonding and that is still unbounded. If the operator doesn't exist or - /// bond creator is not authorized as an operator contract or it is not - /// authorized by the operator or there is no secondary authorization for - /// the provided sortition pool, function returns 0. - /// @dev Implements function expected by sortition pools' IBonding interface. - /// @param operator Address of the operator. - /// @param bondCreator Address authorized to create a bond. - /// @param authorizedSortitionPool Address of authorized sortition pool. - /// @return Amount of authorized wei deposit available for bonding. - function availableUnbondedValue( - address operator, - address bondCreator, - address authorizedSortitionPool - ) public view returns (uint256) { - // Sortition pools check this condition and skips operators that - // are no longer eligible. We cannot revert here. - if ( - registry.isApprovedOperatorContract(bondCreator) && - tokenStaking.isAuthorizedForOperator(operator, bondCreator) && - hasSecondaryAuthorization(operator, authorizedSortitionPool) - ) { - return unbondedValue[operator]; - } - - return 0; - } - /// @notice Withdraws amount from operator's value available for bonding. /// Should not be used by grantee of managed grants. For this case, /// please use `withdrawAsManagedGrantee`. @@ -151,7 +53,7 @@ contract KeepBonding { function withdraw(uint256 amount, address operator) public { require( msg.sender == operator || - msg.sender.isTokenOwnerForOperator(operator, tokenStaking) || + msg.sender.isTokenOwnerForOperator(operator, staking) || msg.sender.isGranteeForOperator(operator, tokenGrant), "Only operator or the owner is allowed to withdraw bond" ); @@ -180,219 +82,4 @@ contract KeepBonding { withdrawBond(amount, operator); } - - /// @notice Create bond for the given operator, holder, reference and amount. - /// @dev Function can be executed only by authorized contract. Reference ID - /// should be unique for holder and operator. - /// @param operator Address of the operator to bond. - /// @param holder Address of the holder of the bond. - /// @param referenceID Reference ID used to track the bond by holder. - /// @param amount Value to bond in wei. - /// @param authorizedSortitionPool Address of authorized sortition pool. - function createBond( - address operator, - address holder, - uint256 referenceID, - uint256 amount, - address authorizedSortitionPool - ) public { - require( - availableUnbondedValue( - operator, - msg.sender, - authorizedSortitionPool - ) >= amount, - "Insufficient unbonded value" - ); - - bytes32 bondID = keccak256( - abi.encodePacked(operator, holder, referenceID) - ); - - require( - lockedBonds[bondID] == 0, - "Reference ID not unique for holder and operator" - ); - - unbondedValue[operator] = unbondedValue[operator].sub(amount); - lockedBonds[bondID] = lockedBonds[bondID].add(amount); - - emit BondCreated( - operator, - holder, - authorizedSortitionPool, - referenceID, - amount - ); - } - - /// @notice Returns value of wei bonded for the operator. - /// @param operator Address of the operator. - /// @param holder Address of the holder of the bond. - /// @param referenceID Reference ID of the bond. - /// @return Amount of wei in the selected bond. - function bondAmount(address operator, address holder, uint256 referenceID) - public - view - returns (uint256) - { - bytes32 bondID = keccak256( - abi.encodePacked(operator, holder, referenceID) - ); - - return lockedBonds[bondID]; - } - - /// @notice Reassigns a bond to a new holder under a new reference. - /// @dev Function requires that a caller is the current holder of the bond - /// which is being reassigned. - /// @param operator Address of the bonded operator. - /// @param referenceID Reference ID of the bond. - /// @param newHolder Address of the new holder of the bond. - /// @param newReferenceID New reference ID to register the bond. - function reassignBond( - address operator, - uint256 referenceID, - address newHolder, - uint256 newReferenceID - ) public { - address holder = msg.sender; - bytes32 bondID = keccak256( - abi.encodePacked(operator, holder, referenceID) - ); - - require(lockedBonds[bondID] > 0, "Bond not found"); - - bytes32 newBondID = keccak256( - abi.encodePacked(operator, newHolder, newReferenceID) - ); - - require( - lockedBonds[newBondID] == 0, - "Reference ID not unique for holder and operator" - ); - - lockedBonds[newBondID] = lockedBonds[bondID]; - lockedBonds[bondID] = 0; - - emit BondReassigned(operator, referenceID, newHolder, newReferenceID); - } - - /// @notice Releases the bond and moves the bond value to the operator's - /// unbounded value pool. - /// @dev Function requires that caller is the holder of the bond which is - /// being released. - /// @param operator Address of the bonded operator. - /// @param referenceID Reference ID of the bond. - function freeBond(address operator, uint256 referenceID) public { - address holder = msg.sender; - bytes32 bondID = keccak256( - abi.encodePacked(operator, holder, referenceID) - ); - - require(lockedBonds[bondID] > 0, "Bond not found"); - - uint256 amount = lockedBonds[bondID]; - lockedBonds[bondID] = 0; - unbondedValue[operator] = unbondedValue[operator].add(amount); - - emit BondReleased(operator, referenceID); - } - - /// @notice Seizes the bond by moving some or all of the locked bond to the - /// provided destination address. - /// @dev Function requires that a caller is the holder of the bond which is - /// being seized. - /// @param operator Address of the bonded operator. - /// @param referenceID Reference ID of the bond. - /// @param amount Amount to be seized. - /// @param destination Address to send the amount to. - function seizeBond( - address operator, - uint256 referenceID, - uint256 amount, - address payable destination - ) public { - require(amount > 0, "Requested amount should be greater than zero"); - - address payable holder = msg.sender; - bytes32 bondID = keccak256( - abi.encodePacked(operator, holder, referenceID) - ); - - require( - lockedBonds[bondID] >= amount, - "Requested amount is greater than the bond" - ); - - lockedBonds[bondID] = lockedBonds[bondID].sub(amount); - - (bool success, ) = destination.call.value(amount)(""); - require(success, "Transfer failed"); - - emit BondSeized(operator, referenceID, destination, amount); - } - - /// @notice Authorizes sortition pool for the provided operator. - /// Operator's authorizers need to authorize individual sortition pools - /// per application since they may be interested in participating only in - /// a subset of keep types used by the given application. - /// @dev Only operator's authorizer can call this function. - function authorizeSortitionPoolContract( - address _operator, - address _poolAddress - ) public { - require( - tokenStaking.authorizerOf(_operator) == msg.sender, - "Not authorized" - ); - authorizedPools[_operator][_poolAddress] = true; - } - - /// @notice Deauthorizes sortition pool for the provided operator. - /// Authorizer may deauthorize individual sortition pool in case the - /// operator should no longer be eligible for work selection and the - /// application represented by the sortition pool should no longer be - /// eligible to create bonds for the operator. - /// @dev Only operator's authorizer can call this function. - function deauthorizeSortitionPoolContract( - address _operator, - address _poolAddress - ) public { - require( - tokenStaking.authorizerOf(_operator) == msg.sender, - "Not authorized" - ); - authorizedPools[_operator][_poolAddress] = false; - } - - /// @notice Checks if the sortition pool has been authorized for the - /// provided operator by its authorizer. - /// @dev See authorizeSortitionPoolContract. - function hasSecondaryAuthorization(address _operator, address _poolAddress) - public - view - returns (bool) - { - return authorizedPools[_operator][_poolAddress]; - } - - /// @notice Withdraws the provided amount from unbonded value of the - /// provided operator to operator's beneficiary. If there is no enough - /// unbonded value or the transfer failed, function fails. - function withdrawBond(uint256 amount, address operator) internal { - require( - unbondedValue[operator] >= amount, - "Insufficient unbonded value" - ); - - unbondedValue[operator] = unbondedValue[operator].sub(amount); - - address beneficiary = tokenStaking.beneficiaryOf(operator); - - (bool success, ) = beneficiary.call.value(amount)(""); - require(success, "Transfer failed"); - - emit UnbondedValueWithdrawn(operator, beneficiary, amount); - } }