Skip to content
Open
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ test-reclaiming:
@cd packages/fastbtc-contracts && make
@integration_test/scripts/test_user_reclaiming.sh

.PHONY: test-withdrawer
test-withdrawer:
@cd packages/fastbtc-contracts && make
@integration_test/scripts/test_withdrawer.sh

.PHONY: run-testnet
run-testnet:
@docker-compose -f docker-compose-base.yml -f docker-compose-testnet.yml up --build
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ $ make run-demo-regtest-slow-replenisher
```


#### Withdrawer (FastBTC-in ManagedWallet replenishment)

```
# In one tab:
$ make run-demo-regtest
# In another tab:
$ make test-withdrawer
```


### Advanced details

The test setup (launched with `make run-demo-regtest`) will expose the Hardhat RPC server at `http://localhost:18545`
Expand Down
4 changes: 4 additions & 0 deletions integration_test/nodes/docker-env-common
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ FASTBTC_BTC_RPC_URL=http://host.docker.internal:18543/wallet/multisig
FASTBTC_BTC_RPC_USERNAME=fastbtc
FASTBTC_BTC_RPC_PASSWORD=hunter2
FASTBTC_BTC_MASTER_PUBLIC_KEYS=tpubD6NzVbkrYhZ4WokHnVXX8CVBt1S88jkmeG78yWbLxn7Wd89nkNDe2J8b6opP4K38mRwXf9d9VVN5uA58epPKjj584R1rnDDbk6oHUD1MoWD,tpubD6NzVbkrYhZ4WpZfRZip3ALqLpXhHUbe6UyG8iiTzVDuvNUyysyiUJWejtbszZYrDaUM8UZpjLmHyvtV7r1QQNFmTqciAz1fYSYkw28Ux6y,tpubD6NzVbkrYhZ4WQZnWqU8ieBsujhoZKZLF6wMvTApJ4ZiGmipk481DyM2su3y5BDeB9fFLwSmmmsGDGJum79he2fnuQMnpWhe3bGir7Mf4uS

FASTBTC_WITHDRAWER_CONTRACT_ADDRESS=0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
FASTBTC_WITHDRAWER_THRESHOLD=10.0
FASTBTC_WITHDRAWER_MAX_AMOUNT=10.0
21 changes: 21 additions & 0 deletions integration_test/scripts/test_withdrawer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
THIS_DIR=$(pwd)
FASTBTC_BRIDGE=0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9
FASTBTC_IN=0x0000000000000000000000000000000000001337

cd ../../packages/fastbtc-contracts
echo "User BTC balance before: $($THIS_DIR/bitcoin-cli.sh -rpcwallet=user getbalance) BTC"
echo "FastBTCBridge rBTC balance before: $(npx hardhat --network integration-test get-rbtc-balance $FASTBTC_BRIDGE) rBTC"
echo "FastBTC-in rBTC balance before: $(npx hardhat --network integration-test get-rbtc-balance $FASTBTC_IN) rBTC"
NUM_TRANSFERS=4
npx hardhat --network integration-test free-money 0xB3b77A8Bc6b6fD93D591C0F34f202eC02e9af2e8 5
npx hardhat --network integration-test transfer-rbtc-to-btc 0xc1daad254b7005eca65780d47213d3de15bd92fcce83777487c5082c6d27600a bcrt1qq8zjw66qrgmynrq3gqdx79n7fcchtaudq4rrf0 0.5 --bridge-address 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 --repeat $NUM_TRANSFERS

echo "$NUM_TRANSFERS transfers sent. They should be visible in a couple of minutes, and replenishment of FastBTC-in ($FASTBTC_IN) should also take place."
echo "Polling balances, Ctrl-C to exit"
while true ; do
echo "User BTC: $($THIS_DIR/bitcoin-cli.sh -rpcwallet=user getbalance) FastBTCBridge rBTC: $(npx hardhat --network integration-test get-rbtc-balance $FASTBTC_BRIDGE) FastBTC-in rBTC: $(npx hardhat --network integration-test get-rbtc-balance $FASTBTC_IN)"
sleep 10
done
2 changes: 1 addition & 1 deletion packages/fastbtc-contracts/contracts/FastBTCBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ contract FastBTCBridge is ReentrancyGuard, FastBTCAccessControllable, Pausable,
{
require(
amount <= totalAdminWithdrawableRbtc,
"Can only withdraw unsent transfers"
"Can only withdraw sent transfers"
);
totalAdminWithdrawableRbtc -= amount;
receiver.sendValue(amount);
Expand Down
150 changes: 150 additions & 0 deletions packages/fastbtc-contracts/contracts/Withdrawer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "./FastBTCAccessControllable.sol";
import "./interfaces/IWithdrawerFastBTCBridge.sol";
import "./interfaces/IWithdrawerFastBTCAccessControl.sol";

/// @title Contract for withdrawing balances to another contract, e.g. the FastBTC-in ManagedWallet contract
/// @notice This contract is set as an admin to the FastBTCBridge contract, and can then be used by federators
/// to withdraw balances to a pre-set contract.
contract Withdrawer is FastBTCAccessControllable {
/// @dev Emitted when rBTC is withdrawn.
event Withdrawal(
uint256 amount
);

/// @dev Emitted when the max amount that can be withdrawn in a single transaction is changed.
event MaxWithdrawableUpdated(
uint256 newMaxWithdrawable
);

/// @dev Emitted when the min time between withdrawals is changed.
event MinTimeBetweenWithdrawalsUpdated(
uint256 newMinTimeBetweenWithdrawals
);

/// @dev The FastBTCBridge contract.
IWithdrawerFastBTCBridge public immutable fastBtcBridge;

/// @dev The address the rBTC is withdrawn to. Intentionally non-changeable for security.
address payable public immutable receiver;

/// @dev Max amount withdrawable in a single transaction
uint256 public maxWithdrawable = 10 ether;

/// @dev Minimum time that has to pass between withdrawals
uint256 public minTimeBetweenWithdrawals = 1 days;

/// @dev Last time the contract was withdrawn from
uint256 public lastWithdrawTimestamp = 0;

constructor(
IWithdrawerFastBTCBridge _fastBtcBridge,
address payable _receiver
)
FastBTCAccessControllable(_fastBtcBridge.accessControl())
{
fastBtcBridge = _fastBtcBridge;
receiver = _receiver;
}

// MAIN API
// ========

/// @dev Withdraw rBTC from the contract to the pre-set receiver. Can only be called by federators.
/// @notice This intentionally only requires a single federator, as
/// @param amount The amount of rBTC to withdraw (in wei).
function withdrawRbtcToReceiver(
uint256 amount
)
external
onlyFederator
{
require(amount > 0, "cannot withdraw zero amount");
require(amount <= maxWithdrawable, "amount too high");
require(block.timestamp - lastWithdrawTimestamp >= minTimeBetweenWithdrawals, "too soon");

lastWithdrawTimestamp = block.timestamp;

fastBtcBridge.withdrawRbtc(amount, receiver);

emit Withdrawal(amount);
}

// ADMIN API
// =========

/// @dev Set the max amount that can be withdrawn in a single transaction.
/// Can only be called by admins.
/// @param _maxWithdrawable The max amount that can be withdrawn in a single transaction.
function setMaxWithdrawable(
uint256 _maxWithdrawable
)
external
onlyAdmin
{
if (_maxWithdrawable == maxWithdrawable) {
return;
}
maxWithdrawable = _maxWithdrawable;
emit MaxWithdrawableUpdated(_maxWithdrawable);
}

/// @dev Set the min time between withdrawals.
/// Can only be called by admins.
/// @param _minTimeBetweenWithdrawals The min time between withdrawals.
function setMinTimeBetweenWithdrawals(
uint256 _minTimeBetweenWithdrawals
)
external
onlyAdmin
{
if (_minTimeBetweenWithdrawals == minTimeBetweenWithdrawals) {
return;
}
minTimeBetweenWithdrawals = _minTimeBetweenWithdrawals;
emit MinTimeBetweenWithdrawalsUpdated(_minTimeBetweenWithdrawals);
}

// PUBLIC VIEWS
// ============

/// @dev Get the amount of rBTC that can be withdrawn this very moment.
function amountWithdrawable() external view returns (uint256 withdrawable) {
if (!hasWithdrawPermissions()) {
return 0;
}

if (block.timestamp - lastWithdrawTimestamp < minTimeBetweenWithdrawals) {
return 0;
}

/// @dev the older version of FastBTCBridge doesn't have this function, so we will revert to balance check
try fastBtcBridge.totalAdminWithdrawableRbtc() returns (uint256 totalAdminWithdrawableRbtc) {
withdrawable = totalAdminWithdrawableRbtc;
} catch {
withdrawable = address(fastBtcBridge).balance;
}

if (withdrawable > maxWithdrawable) {
withdrawable = maxWithdrawable;
}
}

/// @dev Check if the contract has withdraw permissions.
function hasWithdrawPermissions() public view returns (bool) {
IWithdrawerFastBTCAccessControl control = IWithdrawerFastBTCAccessControl(address(accessControl));
return control.hasRole(control.ROLE_ADMIN(), address(this));
}

/// @dev Get the timestamp of the next time the contract can be withdrawn from.
function nextPossibleWithdrawTimestamp() external view returns (uint256) {
return lastWithdrawTimestamp + minTimeBetweenWithdrawals;
}

/// @dev Get the balance of the receiver address.
function receiverBalance() external view returns (uint256) {
return receiver.balance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

/// @title Interface to FastBTCAccessControl from the point of view of the Withdrawer contract
interface IWithdrawerFastBTCAccessControl {
/// @dev The role that has admin privileges on the contract, with permissions to manage other roles and call
/// admin-only functions.
function ROLE_ADMIN() external view returns(bytes32);

/// @dev Is `role` granted to `account`?
function hasRole(bytes32 role, address account) external view returns (bool);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

/// @title Interface to FastBTCBridge, from the point of view of the Withdrawer contract
interface IWithdrawerFastBTCBridge {
/// @dev return the address of the FastBTCAccessControl contract
function accessControl() external view returns (address);

/// @dev Withdraw rBTC from the contract.
/// Can only be called by admins.
/// @param amount The amount of rBTC to withdraw (in wei).
/// @param receiver The address to send the rBTC to.
function withdrawRbtc(
uint256 amount,
address payable receiver
)
external;

/// @dev The amount of rBTC that is sent to Bitcoin and can thus be withdrawn by admins.
function totalAdminWithdrawableRbtc() external view returns(uint256);
}
56 changes: 56 additions & 0 deletions packages/fastbtc-contracts/deploy/04_deploy_withdrawer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {HardhatRuntimeEnvironment} from 'hardhat/types';
import {DeployFunction} from 'hardhat-deploy/types';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const {deploy} = hre.deployments;
const {deployer} = await hre.getNamedAccounts();

let receiverAddress;
if (hre.network.name === 'hardhat') {
// Just use some random address here, it doesn't matter
receiverAddress = '0x0000000000000000000000000000000000001337';
} else if (hre.network.name === 'rsk') {
// mainnet ManagedWallet
receiverAddress = '0xE43cafBDd6674DF708CE9DFF8762AF356c2B454d';
} else if (hre.network.name === 'rsk-testnet') {
// testnet ManagedWallet
receiverAddress = '0xACBE05e7236F7d073295C99E629620DA58284AaD'
} else {
throw new Error(`Unknown network: ${hre.network.name}`);
}

console.log(`Deploying Withdrawer contract with receiver (ManagedWallet) ${receiverAddress}`);

const fastBtcBridgeDeployment = await hre.deployments.get('FastBTCBridge');
const result = await deploy('Withdrawer', {
from: deployer,
args: [fastBtcBridgeDeployment.address, receiverAddress],
log: true,
});

if (result.newlyDeployed) {
const accessControlDeployment = await hre.deployments.get('FastBTCAccessControl');
const accessControl = await hre.ethers.getContractAt(
'FastBTCAccessControl',
accessControlDeployment.address
);
const adminRole = await accessControl.ROLE_ADMIN();

if (hre.network.name === 'hardhat') {
console.log("Setting Withdrawer as an admin.")
await accessControl.grantRole(adminRole, result.address);
} else {
console.log("\n\n!!! NOTE !!!");
console.log(`Withdrawer contract is deployed to ${result.address}, set the access control manually!`)
const txData = accessControl.interface.encodeFunctionData('grantRole', [
adminRole,
result.address
]);
console.log("To set the permissions, send a transaction with data")
console.log(txData)
console.log(`To the FastBTCAccessControl contract at ${accessControl.address}`);
console.log("\n\n")
}
}
};
export default func;
1 change: 1 addition & 0 deletions packages/fastbtc-contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const INTEGRATION_TEST_ADDRESSES: Record<string, string> = {
'FastBTCAccessControl': '0xe7f1725e7734ce288f8367e1bb143e90bb3f0512',
'BTCAddressValidator': '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0',
'FastBTCBridge': '0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9',
'Withdrawer': '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9',
}
async function getDeploymentAddress(givenAddress: string|undefined, hre: HardhatRuntimeEnvironment, name: string): Promise<string> {
if (givenAddress) {
Expand Down
Loading