Skip to content

Commit acc019e

Browse files
Merge pull request #532 from autonomys/add-treasury-contract
feat: add treasury contract
2 parents cc856de + 7061467 commit acc019e

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
7+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
8+
9+
contract AutoDriveTreasury is Ownable2Step, ReentrancyGuard {
10+
// ExistentialDeposit deposit amount for Mainnet Auto-EVM
11+
uint256 public minimumBalance = 1e12 wei;
12+
13+
event FundsWithdrawn(address indexed recipient, uint256 amount);
14+
event MinimumBalanceUpdated(
15+
uint256 oldMinimumBalance,
16+
uint256 newMinimumBalance
17+
);
18+
19+
error InsufficientBalance(uint256 balance, uint256 minimumRequired);
20+
error InvalidRecipient(address recipient);
21+
error InvalidMinimumBalance(uint256 minimumBalance);
22+
23+
constructor(address initialOwner) Ownable(initialOwner) {}
24+
25+
/**
26+
* @notice Allows the owner to withdraw all funds leaving at least the minimum balance
27+
* @param recipient The address to receive the withdrawn funds
28+
*/
29+
function withdraw(address payable recipient) public onlyOwner nonReentrant {
30+
if (recipient == address(0)) {
31+
revert InvalidRecipient(recipient);
32+
}
33+
34+
uint256 balance = address(this).balance;
35+
if (balance <= minimumBalance) {
36+
revert InsufficientBalance(balance, minimumBalance);
37+
}
38+
39+
uint256 withdrawAmount = balance - minimumBalance;
40+
41+
Address.sendValue(recipient, withdrawAmount);
42+
emit FundsWithdrawn(recipient, withdrawAmount);
43+
}
44+
45+
/**
46+
* @notice Allows the owner to update the minimum balance requirement
47+
* @param newMinimumBalance The new minimum balance to maintain
48+
*/
49+
function setMinimumBalance(uint256 newMinimumBalance) public onlyOwner {
50+
if (newMinimumBalance > address(this).balance) {
51+
revert InvalidMinimumBalance(newMinimumBalance);
52+
}
53+
54+
uint256 oldMinimumBalance = minimumBalance;
55+
minimumBalance = newMinimumBalance;
56+
emit MinimumBalanceUpdated(oldMinimumBalance, newMinimumBalance);
57+
}
58+
59+
/**
60+
* @notice Allows the contract to receive ETH
61+
*/
62+
receive() external payable {}
63+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
import {AutoDriveTreasury} from "../src/AutoDriveTreasury.sol";
7+
8+
contract AutoDriveTreasuryTest is Test {
9+
AutoDriveTreasury public treasury;
10+
11+
function setUp() public {
12+
treasury = new AutoDriveTreasury(address(this));
13+
}
14+
15+
function testWithdrawSuccessLeavesMinimumBalance() public {
16+
uint256 min = treasury.minimumBalance();
17+
uint256 fundAmount = min + 3 ether;
18+
19+
// fund contract
20+
(bool ok, ) = payable(address(treasury)).call{value: fundAmount}("");
21+
assertTrue(ok);
22+
23+
address payable recipient = payable(address(0xB0B));
24+
uint256 beforeRecipient = recipient.balance;
25+
26+
treasury.withdraw(recipient);
27+
28+
assertEq(address(treasury).balance, min);
29+
assertEq(recipient.balance, beforeRecipient + (fundAmount - min));
30+
}
31+
32+
function testWithdrawRevertsWhenBalanceLTEMinimum() public {
33+
uint256 min = treasury.minimumBalance();
34+
35+
// fund to exactly minimum
36+
(bool ok, ) = payable(address(treasury)).call{value: min}("");
37+
assertTrue(ok);
38+
39+
address payable recipient = payable(address(0xB0B));
40+
41+
vm.expectRevert(
42+
abi.encodeWithSelector(
43+
AutoDriveTreasury.InsufficientBalance.selector,
44+
address(treasury).balance,
45+
min
46+
)
47+
);
48+
treasury.withdraw(recipient);
49+
}
50+
51+
function testWithdrawRevertsOnZeroRecipient() public {
52+
uint256 min = treasury.minimumBalance();
53+
uint256 fundAmount = min + 1 ether;
54+
(bool ok, ) = payable(address(treasury)).call{value: fundAmount}("");
55+
assertTrue(ok);
56+
57+
vm.expectRevert(
58+
abi.encodeWithSelector(
59+
AutoDriveTreasury.InvalidRecipient.selector,
60+
address(0)
61+
)
62+
);
63+
treasury.withdraw(payable(address(0)));
64+
}
65+
66+
function testTwoStepOwnershipTransfer() public {
67+
address initialOwner = address(this);
68+
address newOwner = address(0xBEEF);
69+
address stranger = address(0xCAFE);
70+
71+
// Initiate two-step transfer
72+
treasury.transferOwnership(newOwner);
73+
assertEq(treasury.owner(), initialOwner);
74+
assertEq(treasury.pendingOwner(), newOwner);
75+
76+
// Only pending owner can accept
77+
vm.prank(stranger);
78+
vm.expectRevert(
79+
abi.encodeWithSelector(
80+
Ownable.OwnableUnauthorizedAccount.selector,
81+
stranger
82+
)
83+
);
84+
treasury.acceptOwnership();
85+
86+
// Pending owner accepts
87+
vm.prank(newOwner);
88+
treasury.acceptOwnership();
89+
assertEq(treasury.owner(), newOwner);
90+
assertEq(treasury.pendingOwner(), address(0));
91+
92+
// Old owner can no longer use onlyOwner
93+
vm.expectRevert(
94+
abi.encodeWithSelector(
95+
Ownable.OwnableUnauthorizedAccount.selector,
96+
initialOwner
97+
)
98+
);
99+
treasury.transferOwnership(stranger);
100+
101+
// New owner can initiate new transfer
102+
vm.prank(newOwner);
103+
treasury.transferOwnership(stranger);
104+
assertEq(treasury.pendingOwner(), stranger);
105+
}
106+
107+
function testSetMinimumBalanceUpdatesAndRevertsIfAboveBalance() public {
108+
// balance initially 0, so only 0 is allowed
109+
vm.expectRevert(
110+
abi.encodeWithSelector(
111+
AutoDriveTreasury.InvalidMinimumBalance.selector,
112+
1
113+
)
114+
);
115+
treasury.setMinimumBalance(1);
116+
117+
// fund and then set to a value <= balance
118+
(bool ok, ) = payable(address(treasury)).call{value: 2 ether}("");
119+
assertTrue(ok);
120+
121+
uint256 oldMin = treasury.minimumBalance();
122+
uint256 newMin = 1 ether;
123+
treasury.setMinimumBalance(newMin);
124+
assertEq(treasury.minimumBalance(), newMin);
125+
126+
// sanity: minimum changed from previous
127+
assertTrue(oldMin != newMin);
128+
}
129+
130+
receive() external payable {}
131+
}

0 commit comments

Comments
 (0)