Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions contracts/foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib/forge-std": {
"rev": "3b20d60d14b343ee4f908cb8079495c07f5e8981"
}
}
48 changes: 47 additions & 1 deletion contracts/src/Chainvoice.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ contract Chainvoice {
error NotAuthorizedPayer();
error IncorrectNativeValue();
error InsufficientAllowance();
error InvalidNewOwner();
error OwnershipNotPending();

// Storage
struct InvoiceDetails {
Expand All @@ -37,6 +39,7 @@ contract Chainvoice {
address public treasuryAddress;
uint256 public fee; // native fee per invoice
uint256 public accumulatedFees; // native fees accrued (for withdraw)
address public pendingOwner; // Two-step ownership transfer

// Events
event InvoiceCreated(uint256 indexed id, address indexed from, address indexed to, address tokenAddress);
Expand All @@ -46,6 +49,12 @@ contract Chainvoice {
event InvoiceBatchCreated(address indexed creator, address indexed token, uint256 count, uint256[] ids);
event InvoiceBatchPaid(address indexed payer, address indexed token, uint256 count, uint256 totalAmount, uint256[] ids);

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event OwnershipTransferInitiated(address indexed currentOwner, address indexed pendingOwner);
event OwnershipTransferCancelled(address indexed owner, address indexed cancelledPendingOwner);
event FeeUpdated(uint256 indexed previousFee, uint256 indexed newFee);
event TreasuryAddressUpdated(address indexed previousTreasury, address indexed newTreasury);

// Constructor
constructor() {
owner = msg.sender;
Expand Down Expand Up @@ -372,14 +381,51 @@ contract Chainvoice {
return invoices[invoiceId];
}

// ========== Admin ==========
// ========== Admin - Ownership ==========
/// @dev Initiates a two-step ownership transfer process
/// @param newOwner Address of the new owner (must not be zero address)
function initiateOwnershipTransfer(address newOwner) external onlyOwner {
if (newOwner == address(0)) revert InvalidNewOwner();
if (newOwner == owner) revert InvalidNewOwner();

pendingOwner = newOwner;
emit OwnershipTransferInitiated(owner, newOwner);
}

/// @dev Completes the ownership transfer process
/// @dev Only the pending owner can call this function
function acceptOwnership() external {
if (msg.sender != pendingOwner) revert OwnershipNotPending();

address previousOwner = owner;
owner = msg.sender;
pendingOwner = address(0);

emit OwnershipTransferred(previousOwner, msg.sender);
}

/// @dev Cancels the pending ownership transfer
function cancelOwnershipTransfer() external onlyOwner {
if (pendingOwner == address(0)) revert OwnershipNotPending();

address cancelledPending = pendingOwner;
pendingOwner = address(0);

emit OwnershipTransferCancelled(msg.sender, cancelledPending);
}

// ========== Admin - Fee Management ==========
function setFeeAmount(uint256 _fee) external onlyOwner {
uint256 previousFee = fee;
fee = _fee;
emit FeeUpdated(previousFee, _fee);
}

function setTreasuryAddress(address newTreasury) external onlyOwner {
require(newTreasury != address(0), "Zero address");
address previousTreasury = treasuryAddress;
treasuryAddress = newTreasury;
emit TreasuryAddressUpdated(previousTreasury, newTreasury);
}

function withdrawFees() external {
Expand Down
71 changes: 71 additions & 0 deletions contracts/test/Chainvoice.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,75 @@ contract ChainvoiceTest is Test {
vm.prank(bob);
chainvoice.payInvoice{value: 1 ether}(0);
}

/* ------------------------------------------------------------ */
/* OWNERSHIP MANAGEMENT */
/* ------------------------------------------------------------ */

function testInitiateOwnershipTransfer() public {
address newOwner = address(0xC0FFEE);

vm.prank(alice); // alice is not the owner
vm.expectRevert("Only owner can call");
chainvoice.initiateOwnershipTransfer(newOwner);

vm.prank(address(this)); // this is the owner (from setUp)
chainvoice.initiateOwnershipTransfer(newOwner);

assertEq(chainvoice.pendingOwner(), newOwner);
}

function testInitiateOwnershipTransferInvalidAddress() public {
vm.expectRevert(Chainvoice.InvalidNewOwner.selector);
chainvoice.initiateOwnershipTransfer(address(0));

// Try to transfer to self
vm.expectRevert(Chainvoice.InvalidNewOwner.selector);
chainvoice.initiateOwnershipTransfer(address(this));
}

function testAcceptOwnership() public {
address newOwner = address(0xC0FFEE);

chainvoice.initiateOwnershipTransfer(newOwner);

vm.prank(newOwner);
chainvoice.acceptOwnership();

assertEq(chainvoice.owner(), newOwner);
assertEq(chainvoice.pendingOwner(), address(0));
}

function testAcceptOwnershipNotPending() public {
vm.prank(address(0xDEADBEEF));
vm.expectRevert(Chainvoice.OwnershipNotPending.selector);
chainvoice.acceptOwnership();
}

function testCancelOwnershipTransfer() public {
address newOwner = address(0xC0FFEE);

chainvoice.initiateOwnershipTransfer(newOwner);
assertEq(chainvoice.pendingOwner(), newOwner);

chainvoice.cancelOwnershipTransfer();
assertEq(chainvoice.pendingOwner(), address(0));
}

function testCancelOwnershipTransferNoPending() public {
vm.expectRevert(Chainvoice.OwnershipNotPending.selector);
chainvoice.cancelOwnershipTransfer();
}

function testFeeUpdateEvent() public {
uint256 newFee = 0.001 ether;
chainvoice.setFeeAmount(newFee);
assertEq(chainvoice.fee(), newFee);
}

function testTreasuryAddressUpdateEvent() public {
address newTreasury = address(0xdead);
chainvoice.setTreasuryAddress(newTreasury);
assertEq(chainvoice.treasuryAddress(), newTreasury);
}
}