Skip to content
Open
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
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"
}
}
45 changes: 44 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,13 +381,47 @@ 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();

emit OwnershipTransferCancelled(msg.sender, pendingOwner);
pendingOwner = address(0);
}

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

function setTreasuryAddress(address newTreasury) external onlyOwner {
require(newTreasury != address(0), "Zero address");

emit TreasuryAddressUpdated(treasuryAddress, newTreasury);
treasuryAddress = newTreasury;
}

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);
}
}