Skip to content

Commit

Permalink
Added validation of moving funds proposal
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaszslabon committed Dec 13, 2023
1 parent cadead9 commit ed761fd
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 0 deletions.
128 changes: 128 additions & 0 deletions solidity/contracts/bridge/WalletProposalValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ contract WalletProposalValidator {
uint256 redemptionTxFee;
}

/// @notice Helper structure representing a moving funds proposal.
struct MovingFundsProposal {
// 20-byte public key hash of the source wallet.
bytes20 walletPubKeyHash;
// List of 20-byte public key hashes of target wallets.
bytes20[] targetWallets;
// Proposed BTC fee for the entire transaction.
uint256 movingFundsTxFee;
}

/// @notice Helper structure representing a heartbeat proposal.
struct HeartbeatProposal {
// 20-byte public key hash of the target wallet.
Expand Down Expand Up @@ -587,6 +597,124 @@ contract WalletProposalValidator {
return true;
}

function validateMovingFundsProposal(
MovingFundsProposal calldata proposal,
BitcoinTx.UTXO calldata walletMainUtxo
) external view returns (bool) {
Wallets.Wallet memory sourceWallet = bridge.wallets(
proposal.walletPubKeyHash
);

// Make sure the source wallet is eligible for moving funds.
require(
sourceWallet.state == Wallets.WalletState.MovingFunds,
"Source wallet is not in MovingFunds state"
);

require(
sourceWallet.pendingRedemptionsValue == 0,
"Source wallet has pending redemptions"
);

require(
sourceWallet.pendingMovedFundsSweepRequestsCount == 0,
"Source wallet has pending moved funds sweep requests"
);

// Make sure the number of target wallets is correct.
uint64 sourceWalletBtcBalance = getWalletBtcBalance(
sourceWallet.mainUtxoHash,
walletMainUtxo
);

require(
sourceWalletBtcBalance > 0,
"Source wallet BTC balance is zero"
);

uint32 liveWalletsCount = bridge.liveWalletsCount();

(, , , , , uint64 walletMaxBtcTransfer, ) = bridge.walletParameters();

uint256 expectedTargetWalletsCount = Math.min(
liveWalletsCount,
Math.ceilDiv(sourceWalletBtcBalance, walletMaxBtcTransfer)
);

require(expectedTargetWalletsCount > 0, "No target wallets available");

require(
proposal.targetWallets.length == expectedTargetWalletsCount,
"Submitted target wallets count is other than expected"
);

// Make sure the target wallets are Live and are ordered correctly.
uint160 lastProcessedTargetWallet = 0;

for (uint256 i = 0; i < proposal.targetWallets.length; i++) {
bytes20 targetWallet = proposal.targetWallets[i];

require(
targetWallet != proposal.walletPubKeyHash,
"Target wallet is equal to source wallet"
);

require(
uint160(targetWallet) > lastProcessedTargetWallet,
"Target wallet order is incorrect"
);

// slither-disable-next-line calls-loop
require(
bridge.wallets(targetWallet).state == Wallets.WalletState.Live,
"Target wallet is not in Live state"
);

lastProcessedTargetWallet = uint160(targetWallet);
}

// Make sure the proposed fee does not exceed the total fee limit.
(uint64 movingFundsTxMaxTotalFee, , , , , , , , , , ) = bridge
.movingFundsParameters();

require(
proposal.movingFundsTxFee > 0,
"Proposed transaction fee cannot be zero"
);

require(
proposal.movingFundsTxFee <= movingFundsTxMaxTotalFee,
"Proposed transaction fee is too high"
);

return true;
}

function getWalletBtcBalance(
bytes32 walletMainUtxoHash,
BitcoinTx.UTXO calldata walletMainUtxo
) internal view returns (uint64 walletBtcBalance) {
// If the wallet has a main UTXO hash set, cross-check it with the
// provided plain-text parameter and get the transaction output value
// as BTC balance. Otherwise, the BTC balance is just zero.
if (walletMainUtxoHash != bytes32(0)) {
require(
keccak256(
abi.encodePacked(
walletMainUtxo.txHash,
walletMainUtxo.txOutputIndex,
walletMainUtxo.txOutputValue
)
) == walletMainUtxoHash,
"Invalid wallet main UTXO data"
);

walletBtcBalance = walletMainUtxo.txOutputValue;
}

return walletBtcBalance;
}

/// @notice View function encapsulating the main rules of a valid heartbeat
/// proposal. This function is meant to facilitate the off-chain
/// validation of the incoming proposals. Thanks to it, most
Expand Down
85 changes: 85 additions & 0 deletions solidity/test/bridge/WalletProposalValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,91 @@ describe("WalletProposalValidator", () => {
})
})

describe("validateMovingFundsProposal", () => {
const walletPubKeyHash = "0x7ac2d9378a1c47e589dfb8095ca95ed2140d2726"
const ecdsaWalletID =
"0x4ad6b3ccbca81645865d8d0d575797a15528e98ced22f29a6f906d3259569863"

before(async () => {
await createSnapshot()

// TODO: Fill with appropriate parameters.
bridge.movingFundsParameters.returns([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
})

after(async () => {
bridge.movingFundsParameters.reset()

await restoreSnapshot()
})

context("when wallet's state is not MovingFunds", () => {
const testData = [
{
testName: "when wallet state is Unknown",
walletState: walletState.Unknown,
},
{
testName: "when wallet state is Live",
walletState: walletState.Live,
},
{
testName: "when wallet state is Closing",
walletState: walletState.Closing,
},
{
testName: "when wallet state is Closed",
walletState: walletState.Closed,
},
{
testName: "when wallet state is Terminated",
walletState: walletState.Terminated,
},
]

testData.forEach((test) => {
context(test.testName, () => {
before(async () => {
await createSnapshot()

bridge.wallets.whenCalledWith(walletPubKeyHash).returns({
ecdsaWalletID,
mainUtxoHash: HashZero,
pendingRedemptionsValue: 0,
createdAt: 0,
movingFundsRequestedAt: 0,
closingStartedAt: 0,
pendingMovedFundsSweepRequestsCount: 0,
state: test.walletState,
movingFundsTargetWalletsCommitmentHash: HashZero,
})
})

after(async () => {
bridge.wallets.reset()

await restoreSnapshot()
})

it("should revert", async () => {
await expect(
// Only walletPubKeyHash argument is relevant in this scenario.
walletProposalValidator.validateMovingFundsProposal({
walletPubKeyHash,
movingFundsTxFee: 0,
targetWallets: [],
})
).to.be.revertedWith("Source wallet is not in MovingFunds state")
})
})
})
})

context("when wallet's state is MovingFunds", () => {
// TODO: Implement
})
})

describe("validateHeartbeatProposal", () => {
context("when message is not valid", () => {
it("should revert", async () => {
Expand Down

0 comments on commit ed761fd

Please sign in to comment.