diff --git a/contracts/access/MultiOwnable.sol b/contracts/access/MultiOwnable.sol index 4e432807..0f9078d6 100644 --- a/contracts/access/MultiOwnable.sol +++ b/contracts/access/MultiOwnable.sol @@ -74,6 +74,15 @@ abstract contract MultiOwnable is IMultiOwnable, Initializable { return _owners.values(); } + /** + * @notice The function to get the address of the primary owner. + * @dev The function ensures compatibility with protocols like OpenSea where a single owner is required + * @return the address of the primary owner + */ + function owner() public view virtual returns (address) { + return _owners.at(0); + } + /** * @notice The function to check the ownership of a user * @param address_ the user to check @@ -89,10 +98,16 @@ abstract contract MultiOwnable is IMultiOwnable, Initializable { * @param newOwners_ the array of addresses to add to _owners */ function _addOwners(address[] memory newOwners_) private { + bool isSetEmpty_ = _owners.length() == 0; + _owners.add(newOwners_); require(!_owners.contains(address(0)), "MultiOwnable: zero address can not be added"); + if (isSetEmpty_) { + emit OwnershipTransferred(address(0), _owners.at(0)); + } + emit OwnersAdded(newOwners_); } @@ -106,8 +121,16 @@ abstract contract MultiOwnable is IMultiOwnable, Initializable { * @param oldOwners_ the array of addresses to remove from _owners */ function _removeOwners(address[] memory oldOwners_) private { + address previousOwner_ = _owners.at(0); + _owners.remove(oldOwners_); + address newOwner_ = _owners.length() > 0 ? _owners.at(0) : address(0); + + if (newOwner_ != previousOwner_) { + emit OwnershipTransferred(previousOwner_, newOwner_); + } + emit OwnersRemoved(oldOwners_); } diff --git a/contracts/interfaces/access/IMultiOwnable.sol b/contracts/interfaces/access/IMultiOwnable.sol index c59bb5b0..19cc79f4 100644 --- a/contracts/interfaces/access/IMultiOwnable.sol +++ b/contracts/interfaces/access/IMultiOwnable.sol @@ -7,6 +7,7 @@ pragma solidity ^0.8.4; interface IMultiOwnable { event OwnersAdded(address[] newOwners); event OwnersRemoved(address[] removedOwners); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @notice The function to add equally rightful owners to the contract diff --git a/package-lock.json b/package-lock.json index 88cf618c..5f8cef30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solarity/solidity-lib", - "version": "2.7.6", + "version": "2.7.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solarity/solidity-lib", - "version": "2.7.6", + "version": "2.7.7", "license": "MIT", "dependencies": { "@openzeppelin/contracts": "4.9.5", diff --git a/package.json b/package.json index f9d25cdb..b0b52671 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/solidity-lib", - "version": "2.7.6", + "version": "2.7.7", "license": "MIT", "author": "Distributed Lab", "readme": "README.md", diff --git a/test/access/MultiOwnable.test.ts b/test/access/MultiOwnable.test.ts index d10d5a66..48cdc7cf 100644 --- a/test/access/MultiOwnable.test.ts +++ b/test/access/MultiOwnable.test.ts @@ -60,6 +60,20 @@ describe("MultiOwnable", () => { expect(await multiOwnable.isOwner(THIRD.address)).to.be.true; }); + it("should emit OwnershipTransferred event when init function is called", async () => { + const MultiOwnableMock = await ethers.getContractFactory("MultiOwnableMock"); + const multiOwnable = await MultiOwnableMock.deploy(); + + await expect(multiOwnable.connect(SECOND).__MultiOwnableMock_init()) + .to.emit(multiOwnable, "OwnershipTransferred") + .withArgs(ZERO_ADDR, SECOND.address); + + await expect(multiOwnable.connect(SECOND).addOwners([SECOND.address, THIRD.address])).not.to.emit( + multiOwnable, + "OwnershipTransferred", + ); + }); + it("should not add null address", async () => { await expect(multiOwnable.addOwners([ZERO_ADDR])).to.be.revertedWith( "MultiOwnable: zero address can not be added", @@ -75,6 +89,21 @@ describe("MultiOwnable", () => { expect(await multiOwnable.isOwner(SECOND.address)).to.be.false; expect(await multiOwnable.isOwner(FIRST.address)).to.be.true; }); + + it("should emit OwnershipTransferred event only when primary owner is changed", async () => { + await multiOwnable.addOwners([SECOND.address]); + + await expect(multiOwnable.connect(SECOND).removeOwners([FIRST.address])) + .to.emit(multiOwnable, "OwnershipTransferred") + .withArgs(FIRST.address, SECOND.address); + + await multiOwnable.connect(SECOND).addOwners([FIRST.address]); + await expect(multiOwnable.removeOwners([FIRST.address])).not.to.emit(multiOwnable, "OwnershipTransferred"); + + await expect(multiOwnable.connect(SECOND).removeOwners([SECOND.address])) + .to.emit(multiOwnable, "OwnershipTransferred") + .withArgs(SECOND.address, ZERO_ADDR); + }); }); describe("renounceOwnership()", () => { @@ -93,6 +122,12 @@ describe("MultiOwnable", () => { }); }); + describe("owner()", () => { + it("should correctly get the primary owner", async () => { + expect(await multiOwnable.owner()).to.be.equal(FIRST.address); + }); + }); + describe("isOwner()", () => { it("should correctly check the initial owner", async () => { expect(await multiOwnable.isOwner(FIRST.address)).to.be.true;