Skip to content

Commit 42a416d

Browse files
authored
Merge pull request #39 from keep-network/signer-bonds-escrow
Signer bonds escrow ETH purchased by the risk manager from tBTC signer bonds needs to be swapped and deposited back to the coverage pool as collateral. In the case it can not be done automatically, the governance has the power to ask the risk manager to deposit ETH from purchased signer bonds into an escrow the governance can later withdraw from and do the swap manually. SignerBondsEscrow is a simple escrow implementation allowing the risk manager to store purchased ETH signer bonds so that governance can later swap them manually and deposit as coverage pool collateral.
2 parents cd020ed + 31c0519 commit 42a416d

6 files changed

Lines changed: 180 additions & 5 deletions

File tree

contracts/RiskManagerV1.sol

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ interface IDeposit {
2323
function currentState() external view returns (uint256);
2424

2525
function lotSizeTbtc() external view returns (uint256);
26+
27+
function withdrawableAmount() external view returns (uint256);
28+
}
29+
30+
/// @title ISignerBondsSwapStrategy
31+
/// @notice Represents a signer bonds swap strategy.
32+
/// @dev This interface is meant to abstract the underlying signer bonds
33+
/// swap strategy and make it interchangeable for the governance.
34+
interface ISignerBondsSwapStrategy {
35+
/// @notice Swaps signer bonds.
36+
function swapSignerBonds() external payable;
2637
}
2738

2839
/// @title RiskManagerV1 for tBTCv1
@@ -41,6 +52,9 @@ contract RiskManagerV1 is Auctioneer, Ownable {
4152

4253
IERC20 public tbtcToken;
4354

55+
// TODO: should be possible to change by the governance.
56+
ISignerBondsSwapStrategy public signerBondsSwapStrategy;
57+
4458
// deposit in liquidation => opened coverage pool auction
4559
mapping(address => address) public depositToAuction;
4660
// opened coverage pool auction => deposit in liquidation
@@ -69,10 +83,12 @@ contract RiskManagerV1 is Auctioneer, Ownable {
6983
constructor(
7084
IERC20 _tbtcToken,
7185
CoveragePool _coveragePool,
86+
ISignerBondsSwapStrategy _signerBondsSwapStrategy,
7287
address _masterAuction,
7388
uint256 _auctionLength
7489
) Auctioneer(_coveragePool, _masterAuction) {
7590
tbtcToken = _tbtcToken;
91+
signerBondsSwapStrategy = _signerBondsSwapStrategy;
7692
auctionLength = _auctionLength;
7793
}
7894

@@ -191,9 +207,11 @@ contract RiskManagerV1 is Auctioneer, Ownable {
191207
// Purchase signers bonds ETH with TBTC acquired from the auction
192208
deposit.purchaseSignerBondsAtAuction();
193209

194-
// TODO: Once ETH is received, funds need to be processed further, so
195-
// they won't be locked in this contract.
210+
uint256 withdrawableAmount = deposit.withdrawableAmount();
196211
deposit.withdrawFunds();
212+
213+
// slither-disable-next-line arbitrary-send
214+
signerBondsSwapStrategy.swapSignerBonds{value: withdrawableAmount}();
197215
}
198216

199217
/// @notice Get the time remaining until the function parameter timer

contracts/SignerBondsEscrow.sol

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity <0.9.0;
4+
5+
import "./RiskManagerV1.sol";
6+
import "@openzeppelin/contracts/access/Ownable.sol";
7+
8+
/// @title SignerBondsEscrow
9+
/// @notice ETH purchased by the risk manager from tBTC signer bonds needs to be
10+
/// swapped and deposited back to the coverage pool as collateral.
11+
/// In the case it can not be done automatically, the governance has
12+
/// the power to ask the risk manager to deposit ETH from purchased
13+
/// signer bonds into an escrow the governance can later withdraw from
14+
/// and do the swap manually. SignerBondsEscrow is a simple escrow
15+
/// implementation allowing the risk manager to store purchased ETH
16+
/// signer bonds so that governance can later swap them manually and
17+
/// deposit as coverage pool collateral.
18+
contract SignerBondsEscrow is ISignerBondsSwapStrategy, Ownable {
19+
/// @notice Swaps signer bonds.
20+
/// @dev Adds incoming bonds to the overall contract balance.
21+
function swapSignerBonds() external payable override {}
22+
23+
/// @notice Withdraws collected bonds to the given target address.
24+
/// @dev Can be called by the governance only.
25+
/// @param recipient Arbitrary recipient address chosen by the governance
26+
/// that will be responsible for swapping ETH and depositing
27+
/// collateral to the coverage pool.
28+
function withdraw(address payable recipient) external onlyOwner {
29+
require(recipient != address(0), "Invalid recipient address");
30+
/* solhint-disable avoid-low-level-calls */
31+
// slither-disable-next-line low-level-calls,arbitrary-send
32+
(bool success, ) = recipient.call{value: address(this).balance}("");
33+
require(success, "Failed to send Ether");
34+
/* solhint-enable avoid-low-level-calls */
35+
}
36+
}

contracts/test/DepositStub.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,8 @@ contract DepositStub is IDeposit {
6868
function notifyUndercollateralizedLiquidation() external {
6969
currentState = uint256(States.LIQUIDATION_IN_PROGRESS);
7070
}
71+
72+
function withdrawableAmount() external view override returns (uint256) {
73+
return address(this).balance;
74+
}
7175
}

test/RiskManagerV1.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const auctionLength = 86400 // 24h
1717

1818
describe("RiskManagerV1", () => {
1919
let testToken
20+
let signerBondsSwapStrategy
2021
let owner
2122
let notifier
2223
let riskManagerV1
@@ -27,6 +28,12 @@ describe("RiskManagerV1", () => {
2728
testToken = await TestToken.deploy()
2829
await testToken.deployed()
2930

31+
const SignerBondsSwapStrategy = await ethers.getContractFactory(
32+
"SignerBondsEscrow"
33+
)
34+
signerBondsSwapStrategy = await SignerBondsSwapStrategy.deploy()
35+
await signerBondsSwapStrategy.deployed()
36+
3037
const Auction = await ethers.getContractFactory("Auction")
3138
const CoveragePoolStub = await ethers.getContractFactory("CoveragePoolStub")
3239
const coveragePoolStub = await CoveragePoolStub.deploy()
@@ -39,6 +46,7 @@ describe("RiskManagerV1", () => {
3946
riskManagerV1 = await RiskManagerV1.deploy(
4047
testToken.address,
4148
coveragePoolStub.address,
49+
signerBondsSwapStrategy.address,
4250
masterAuction.address,
4351
auctionLength
4452
)

test/SignerBondsEscrow.test.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const chai = require("chai")
2+
const expect = chai.expect
3+
4+
const { ZERO_ADDRESS } = require("./helpers/contract-test-helpers")
5+
6+
describe("SignerBondsEscrow", () => {
7+
let governance
8+
let riskManager
9+
let signerBondsEscrow
10+
11+
beforeEach(async () => {
12+
governance = await ethers.getSigner(0)
13+
riskManager = await ethers.getSigner(1)
14+
15+
const SignerBondsEscrow = await ethers.getContractFactory(
16+
"SignerBondsEscrow"
17+
)
18+
signerBondsEscrow = await SignerBondsEscrow.deploy()
19+
await signerBondsEscrow.deployed()
20+
})
21+
22+
describe("swapSignerBonds", () => {
23+
let tx
24+
25+
beforeEach(async () => {
26+
tx = await signerBondsEscrow
27+
.connect(riskManager)
28+
.swapSignerBonds({ value: ethers.utils.parseEther("10") })
29+
})
30+
31+
it("should add the processed signer bonds to the contract balance", async () => {
32+
await expect(tx).to.changeEtherBalance(
33+
signerBondsEscrow,
34+
ethers.utils.parseEther("10")
35+
)
36+
})
37+
})
38+
39+
describe("withdraw", () => {
40+
context(
41+
"when the caller is the governance, target address is non-zero, " +
42+
"and there are available funds",
43+
() => {
44+
let tx
45+
46+
beforeEach(async () => {
47+
await signerBondsEscrow
48+
.connect(riskManager)
49+
.swapSignerBonds({ value: ethers.utils.parseEther("10") })
50+
51+
tx = await signerBondsEscrow
52+
.connect(governance)
53+
.withdraw(riskManager.address)
54+
})
55+
56+
it("transfer all funds to the target account", async () => {
57+
await expect(tx).to.changeEtherBalance(
58+
signerBondsEscrow,
59+
ethers.utils.parseEther("10").mul(-1)
60+
)
61+
await expect(tx).to.changeEtherBalance(
62+
riskManager,
63+
ethers.utils.parseEther("10")
64+
)
65+
})
66+
}
67+
)
68+
69+
context("when the caller is not the governance", () => {
70+
it("should revert", async () => {
71+
await expect(
72+
signerBondsEscrow.connect(riskManager).withdraw(riskManager.address)
73+
).to.be.revertedWith("Ownable: caller is not the owner")
74+
})
75+
})
76+
77+
context("when the target address is zero", () => {
78+
it("should revert", async () => {
79+
await expect(
80+
signerBondsEscrow.connect(governance).withdraw(ZERO_ADDRESS)
81+
).to.be.revertedWith("Invalid recipient address")
82+
})
83+
})
84+
85+
context("when there are no available funds", () => {
86+
it("should not transfer any funds", async () => {
87+
const tx = await signerBondsEscrow
88+
.connect(governance)
89+
.withdraw(riskManager.address)
90+
await expect(tx).to.changeEtherBalance(signerBondsEscrow, 0)
91+
await expect(tx).to.changeEtherBalance(riskManager, 0)
92+
})
93+
})
94+
})
95+
})

test/liquidation/liquidation.test.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ describe("Integration -- liquidation happy path", () => {
88
const bondedAmount = to1e18(150)
99

1010
let tbtcToken
11+
let signerBondsSwapStrategy
1112
let coveragePool
1213
let riskManagerV1
1314
let tbtcDeposit
@@ -19,6 +20,12 @@ describe("Integration -- liquidation happy path", () => {
1920
tbtcToken = await TestToken.deploy()
2021
await tbtcToken.deployed()
2122

23+
const SignerBondsSwapStrategy = await ethers.getContractFactory(
24+
"SignerBondsEscrow"
25+
)
26+
signerBondsSwapStrategy = await SignerBondsSwapStrategy.deploy()
27+
await signerBondsSwapStrategy.deployed()
28+
2229
const Auction = await ethers.getContractFactory("Auction")
2330

2431
const masterAuction = await Auction.deploy()
@@ -32,6 +39,7 @@ describe("Integration -- liquidation happy path", () => {
3239
riskManagerV1 = await RiskManagerV1.deploy(
3340
tbtcToken.address,
3441
coveragePool.address,
42+
signerBondsSwapStrategy.address,
3543
masterAuction.address,
3644
auctionLength
3745
)
@@ -72,13 +80,19 @@ describe("Integration -- liquidation happy path", () => {
7280
expect(await auction.isOpen()).to.be.false
7381
})
7482

75-
it("should purchase and withdraw signer bonds from deposit", async () => {
76-
// Risk Manager has all ETH bonds at their disposal
77-
await expect(tx).to.changeEtherBalance(riskManagerV1, bondedAmount)
83+
it("should liquidate the deposit", async () => {
7884
// Auction bidder has spend their TBTC
7985
expect(await tbtcToken.balanceOf(bidder.address)).to.equal(0)
8086
// Deposit has been liquidated
8187
expect(await tbtcDeposit.currentState()).to.equal(11) // LIQUIDATED
8288
})
89+
90+
it("should swap signer bonds", async () => {
91+
await expect(tx).to.changeEtherBalance(riskManagerV1, 0)
92+
await expect(tx).to.changeEtherBalance(
93+
signerBondsSwapStrategy,
94+
bondedAmount
95+
)
96+
})
8397
})
8498
})

0 commit comments

Comments
 (0)