Skip to content
This repository has been archived by the owner on Mar 28, 2023. It is now read-only.

Escape Hatch: Add a way for owners of a funding-failed deposit to ask nicely for their UTXO(s) back #583

Merged
merged 7 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion solidity/contracts/DepositLog.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,21 @@ contract DepositLog {
uint256 _timestamp
);

// This event is fired when we enter the SETUP_FAILED state for any reason
// This event is fired when we enter the FAILED_SETUP state for any reason
event SetupFailed(
address indexed _depositContractAddress,
uint256 _timestamp
);

// This event is fired when a funder requests funder abort after
// FAILED_SETUP has been reached. Funder abort is a voluntary signer action
// to return UTXO(s) that were sent to a signer-controlled wallet despite
// the funding proofs having failed.
event FunderAbortRequested(
address indexed _depositContractAddress,
bytes _abortOutputScript
);

// This event is fired when we detect an ECDSA fraud before seeing a funding proof
event FraudDuringSetup(
address indexed _depositContractAddress,
Expand Down Expand Up @@ -210,6 +219,16 @@ contract DepositLog {
emit SetupFailed(msg.sender, block.timestamp);
}

/// @notice Fires a FunderAbortRequested event.
/// @dev We append the sender, which is the deposit contract that called.
function logFunderRequestedAbort(bytes memory _abortOutputScript) public {
require(
approvedToLog(msg.sender),
"Caller is not approved to log events"
);
emit FunderAbortRequested(msg.sender, _abortOutputScript);
}

/// @notice Fires a FraudDuringSetup event.
/// @dev We append the sender, which is the deposit contract that called.
function logFraudDuringSetup() public {
Expand Down
23 changes: 22 additions & 1 deletion solidity/contracts/deposit/Deposit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ contract Deposit is DepositFactoryAuthority {
}

function () external payable {
require(msg.sender == self.keepAddress, "Deposit contract can only receive ETH from rom underlying keep");
require(msg.sender == self.keepAddress, "Deposit contract can only receive ETH from underlying keep");
}

/// @notice Get the integer representing the current state.
Expand Down Expand Up @@ -282,6 +282,27 @@ contract Deposit is DepositFactoryAuthority {
return true;
}

/// @notice Requests a funder abort for a failed-funding deposit; that is,
/// requests the return of a sent UTXO to _abortOutputScript. It
/// imposes no requirements on the signing group. Signers should
/// send their UTXO to the requested output script, but do so at
/// their discretion and with no penalty for failing to do so. This
/// can be used for example when a UTXO is sent that is the wrong
/// size for the lot.
/// @dev This is a self-admitted funder fault, and is only be callable by
/// the TDT holder. This function emits the FunderAbortRequested event,
/// but stores no additional state.
/// @param _abortOutputScript The output script the funder wishes to request
/// a return of their UTXO to.
function requestFunderAbort(bytes memory _abortOutputScript) public {
require(
self.depositOwner() == msg.sender,
"Only TDT holder can request funder abort"
);

self.requestFunderAbort(_abortOutputScript);
}

/// @notice Anyone can provide a signature that was not requested to prove fraud during funding.
/// @dev Calls out to the keep to verify if there was fraud.
/// @param _v Signature recovery value.
Expand Down
26 changes: 25 additions & 1 deletion solidity/contracts/deposit/DepositFunding.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ library DepositFunding {
}

/// @notice Anyone may notify the contract that the funder has failed to send BTC.
/// @dev This is considered a funder fault, and we revoke their bond.
/// @dev This is considered a funder fault, and the funder's payment
/// for opening the deposit is not refunded.
/// @param _d Deposit storage pointer.
function notifyFundingTimeout(DepositUtils.Deposit storage _d) public {
require(_d.inAwaitingBTCFundingProof(), "Funding timeout has not started");
Expand All @@ -143,6 +144,29 @@ library DepositFunding {
fundingTeardown(_d);
}

/// @notice Requests a funder abort for a failed-funding deposit; that is,
/// requests return of a sent UTXO to `_abortOutputScript`. This can
/// be used for example when a UTXO is sent that is the wrong size
/// for the lot. Must be called after setup fails for any reason,
/// and imposes no requirement or incentive on the signing group to
/// return the UTXO.
/// @dev This is a self-admitted funder fault, and should only be callable
/// by the TDT holder.
/// @param _d Deposit storage pointer.
/// @param _abortOutputScript The output script the funder wishes to request
/// a return of their UTXO to.
function requestFunderAbort(
DepositUtils.Deposit storage _d,
bytes memory _abortOutputScript
) public {
require(
_d.inFailedSetup(),
"The deposit has not failed funding"
);

_d.logFunderRequestedAbort(_abortOutputScript);
}

/// @notice Anyone can provide a signature that was not requested to prove fraud during funding.
/// @dev Calls out to the keep to verify if there was fraud.
/// @param _d Deposit storage pointer.
Expand Down
12 changes: 6 additions & 6 deletions solidity/contracts/deposit/DepositRedemption.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ library DepositRedemption {
uint256 signerFee = _d.signerFee();
uint256 tbtcOwed = _d.getRedemptionTbtcRequirement(_d.redeemerAddress);

// if we owe 0 TBTC, msg.sender is TDT owner and FRT holder.
// if we owe 0 TBTC, msg.sender is TDT holder and FRT holder.
if(tbtcOwed == 0){
return;
}
// if we owe > 0 & < signerfee, msg.sender is TDT owner but not FRT holder.
// if we owe > 0 & < signerfee, msg.sender is TDT holder but not FRT holder.
if(tbtcOwed <= signerFee){
_d.tbtcToken.transferFrom(msg.sender, address(this), tbtcOwed);
return;
Expand All @@ -70,18 +70,18 @@ library DepositRedemption {
if(tbtcOwed == tbtcLot){
// the TDT holder has exclusive redemption rights to a UXTO up until the deposit’s term.
// At that point, we open it up so anyone may redeem it.
// As compensation, the TDT owner is reimbursed in TBTC
// As compensation, the TDT holder is reimbursed in TBTC
// Vending Machine-owned TDTs have been used to mint TBTC,
// and we should always burn a full TBTC to redeem the deposit.
if(tdtHolder == vendingMachineAddress){
_d.tbtcToken.burnFrom(msg.sender, tbtcLot);
}
// if signer fee is not escrowed, escrow and it here and send the rest to TDT owner
// if signer fee is not escrowed, escrow and it here and send the rest to TDT holder
else if(_d.tbtcToken.balanceOf(address(this)) < signerFee){
_d.tbtcToken.transferFrom(msg.sender, address(this), signerFee);
_d.tbtcToken.transferFrom(msg.sender, tdtHolder, tbtcLot.sub(signerFee));
}
// tansfer a full TBTC to TDT owner if signerFee is escrowed
// tansfer a full TBTC to TDT holder if signerFee is escrowed
else{
_d.tbtcToken.transferFrom(msg.sender, tdtHolder, tbtcLot);
}
Expand Down Expand Up @@ -155,7 +155,7 @@ library DepositRedemption {
_requestRedemption(_d, _outputValueBytes, _redeemerOutputScript, _finalRecipient);
}

/// @notice Only TDT owner can request redemption,
/// @notice Only TDT holder can request redemption,
/// unless Deposit is expired or in COURTESY_CALL.
/// @dev The redeemer specifies details about the Bitcoin redemption transaction.
/// @param _d Deposit storage pointer.
Expand Down
6 changes: 3 additions & 3 deletions solidity/contracts/deposit/DepositUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ library DepositUtils {

// INITIALLY WRITTEN BY REDEMPTION FLOW
address payable redeemerAddress; // The redeemer's address, used as fallback for fraud in redemption
bytes redeemerOutputScript; // The 20-byte redeemer PKH
bytes redeemerOutputScript; // The redeemer output script
uint256 initialRedemptionFee; // the initial fee as requested
uint256 latestRedemptionFee; // the fee currently required by a redemption transaction
uint256 withdrawalRequestTime; // the most recent withdrawal request timestamp
Expand Down Expand Up @@ -442,7 +442,7 @@ library DepositUtils {
}

/// @notice Get TBTC amount required for redemption assuming _redeemer
/// is this deposit's TDT owner.
/// is this deposit's TDT holder.
/// @param _redeemer The assumed owner of the deposit's TDT.
/// @return The amount in TBTC needed to redeem the deposit.
function getOwnerRedemptionTbtcRequirement(DepositUtils.Deposit storage _d, address _redeemer) internal view returns(uint256) {
Expand All @@ -469,7 +469,7 @@ library DepositUtils {
if (depositOwner(_d) == _redeemer && !inCourtesy) {
return getOwnerRedemptionTbtcRequirement(_d, _redeemer);
}
require(remainingTerm(_d) == 0 || inCourtesy, "Only TDT owner can redeem unless deposit is at-term or in COURTESY_CALL");
require(remainingTerm(_d) == 0 || inCourtesy, "Only TDT holder can redeem unless deposit is at-term or in COURTESY_CALL");
return lotSizeTbtc(_d);
}
}
10 changes: 10 additions & 0 deletions solidity/contracts/deposit/OutsourceDepositLogging.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ library OutsourceDepositLogging {
_logger.logSetupFailed();
}

/// @notice Fires a FunderAbortRequested event.
/// @dev The logger is on a system contract, so all logs from all deposits are from the same address.
function logFunderRequestedAbort(
DepositUtils.Deposit storage _d,
bytes memory _abortOutputScript
) public {
DepositLog _logger = DepositLog(address(_d.tbtcSystem));
_logger.logFunderRequestedAbort(_abortOutputScript);
}

/// @notice Fires a FraudDuringSetup event.
/// @dev The logger is on a system contract, so all logs from all deposits are from the same address.
function logFraudDuringSetup(DepositUtils.Deposit storage _d) external {
Expand Down
53 changes: 52 additions & 1 deletion solidity/test/DepositFundingTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ describe("DepositFunding", async function() {
let timer
let owner
let openKeepFee
before(async () => {})

before(async () => {
;({
Expand Down Expand Up @@ -394,6 +393,58 @@ describe("DepositFunding", async function() {
})
})

describe("requestFunderAbort", async () => {
let timer
let owner

before(async () => {
timer = await tbtcConstants.getFundingTimeout.call()
owner = accounts[1]
await tbtcDepositToken.forceMint(
owner,
web3.utils.toBN(testDeposit.address),
)
})

beforeEach(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but we can move all of this to the before block, and then:

    beforeEach(async () => {
      await createSnapshot()
    })

    afterEach(async () => {
      await restoreSnapshot()
    })

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bah, I actually wanted to do this, but I think we're close enough here that I'm going to punt it to an imaginary future improvement.

const block = await web3.eth.getBlock("latest")
const blockTimestamp = block.timestamp
fundingProofTimerStart = blockTimestamp - timer.toNumber() - 1

await testDeposit.setState(states.AWAITING_BTC_FUNDING_PROOF)
await testDeposit.setFundingProofTimerStart(fundingProofTimerStart)
})

it("fails if the deposit has not failed setup", () => {
expectRevert(
testDeposit.requestFunderAbort("0x1234", {from: owner}),
"The deposit has not failed funding",
)
})

it("emits a FunderAbortRequested event", async () => {
const blockNumber = await web3.eth.getBlockNumber()
await testDeposit.notifyFundingTimeout()

const outputScript = "0x012345"
await testDeposit.requestFunderAbort(outputScript, {from: owner})

const eventList = await tbtcSystemStub.getPastEvents(
"FunderAbortRequested",
{
fromBlock: blockNumber,
toBlock: "latest",
},
)
expect(eventList.length).to.equal(1)
expect(eventList[0].name == "FunderAbortRequested")
expect(eventList[0].returnValues).to.contain({
_depositContractAddress: testDeposit.address,
_abortOutputScript: outputScript,
})
})
})

describe("provideBTCFundingProof", async () => {
beforeEach(async () => {
await mockRelay.setCurrentEpochDifficulty(currentDifficulty)
Expand Down
14 changes: 7 additions & 7 deletions solidity/test/DepositRedemptionTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,11 @@ describe("DepositRedemption", async function() {
it("reverts if deposit is pre-term and redeemer is not Deposit owner", async () => {
await expectRevert(
testDeposit.getRedemptionTbtcRequirement.call(accounts[1]),
"Only TDT owner can redeem unless deposit is at-term or in COURTESY_CALL",
"Only TDT holder can redeem unless deposit is at-term or in COURTESY_CALL",
)
})

it("returns full TBTC if we are at-term and caller is not TDT owner", async () => {
it("returns full TBTC if we are at-term and caller is not TDT holder", async () => {
await increaseTime(depositTerm.toNumber())
await tbtcDepositToken.transferFrom(tdtHolder, accounts[1], tdtId, {
from: owner,
Expand All @@ -226,7 +226,7 @@ describe("DepositRedemption", async function() {
expect(tbtcOwed).to.eq.BN(depositValue)
})

it("returns SignerFee if we are at-term, caller is TDT owner, and fee is not escrowed", async () => {
it("returns SignerFee if we are at-term, caller is TDT holder, and fee is not escrowed", async () => {
await increaseTime(depositTerm.toNumber())

const tbtcOwed = await testDeposit.getRedemptionTbtcRequirement.call(
Expand All @@ -235,7 +235,7 @@ describe("DepositRedemption", async function() {
expect(tbtcOwed).to.eq.BN(signerFee)
})

it("returns zero if we are at-term, caller is TDT owner and signer fee is escrowed", async () => {
it("returns zero if we are at-term, caller is TDT holder and signer fee is escrowed", async () => {
await tbtcToken.forceMint(testDeposit.address, signerFee)
await increaseTime(depositTerm.toNumber())

Expand Down Expand Up @@ -309,7 +309,7 @@ describe("DepositRedemption", async function() {
expect(events[0].returnValues.value).to.eq.BN(signerFee)
})

it("burns 1 TBTC if deposit is in COURTESY_CALL and TDT owner is the Vending Machine", async () => {
it("burns 1 TBTC if deposit is in COURTESY_CALL and TDT holder is the Vending Machine", async () => {
const {
receipt: {blockNumber: transferBlock},
} = await tbtcDepositToken.transferFrom(
Expand Down Expand Up @@ -377,7 +377,7 @@ describe("DepositRedemption", async function() {
expect(events[1].returnValues.value).to.eq.BN(depositValue.sub(signerFee))
})

it("transfers 1 TBTC to TDT owner if deposit is in COURTESY_CALL and fee is escrowed", async () => {
it("transfers 1 TBTC to TDT holder if deposit is in COURTESY_CALL and fee is escrowed", async () => {
await tbtcToken.forceMint(testDeposit.address, signerFee)
await testDeposit.setState(states.COURTESY_CALL)

Expand Down Expand Up @@ -641,7 +641,7 @@ describe("DepositRedemption", async function() {
"0x" + "33".repeat(20),
{from: owner},
),
"Only TDT owner can redeem unless deposit is at-term or in COURTESY_CALL",
"Only TDT holder can redeem unless deposit is at-term or in COURTESY_CALL",
)
})
})
Expand Down