diff --git a/src/factories/ImmutableOvalChainlinkFactory.sol b/src/factories/ImmutableOvalChainlinkFactory.sol index 5f10512..bd41031 100644 --- a/src/factories/ImmutableOvalChainlinkFactory.sol +++ b/src/factories/ImmutableOvalChainlinkFactory.sol @@ -6,28 +6,37 @@ import {ChainlinkSourceAdapter} from "../adapters/source-adapters/ChainlinkSourc import {ChainlinkDestinationAdapter} from "../adapters/destination-adapters/ChainlinkDestinationAdapter.sol"; import {IAggregatorV3Source} from "../interfaces/chainlink/IAggregatorV3Source.sol"; +/** + * @title OvalChainlinkImmutable, providing an immutable controller for Oval wapped Chainlink. + */ contract OvalChainlinkImmutable is ImmutableController, ChainlinkSourceAdapter, ChainlinkDestinationAdapter { - constructor( - IAggregatorV3Source source, - address[] memory unlockers, - uint256 _lockWindow, - uint256 _maxTraversal, - address owner - ) + constructor(IAggregatorV3Source source, address[] memory unlockers, uint256 _lockWindow, uint256 _maxTraversal) ChainlinkSourceAdapter(source) ImmutableController(_lockWindow, _maxTraversal, unlockers) ChainlinkDestinationAdapter(source.decimals()) {} } -contract MutableUnlockersOvalChainlinkFactory { +/** + * @title ImmutableUnlockersOvalChainlinkFactory + * @dev Factory contract to create instances of OvalChainlinkImmutable. If Oval instances are deployed from this factory + * then downstream contracts can be sure the inheretence structure is defined correctly. + */ +contract ImmutableUnlockersOvalChainlinkFactory { + /** + * @dev Creates an instance of OvalChainlinkImmutable. + * @param source The Chainlink source aggregator. This is the address of the contract to be wrapped by Oval. + * @param lockWindow The time window during which the unlockers can operate. + * @param maxTraversal The maximum number of historical data points to traverse. + * @param unlockers Array of addresses that can unlock the controller. + * @return The address of the newly created OvalChainlinkImmutable instance. + */ function createImmutableOvalChainlink( IAggregatorV3Source source, uint256 lockWindow, uint256 maxTraversal, - address owner, address[] memory unlockers ) external returns (address) { - return address(new OvalChainlinkImmutable(source, unlockers, lockWindow, maxTraversal, owner)); + return address(new OvalChainlinkImmutable(source, unlockers, lockWindow, maxTraversal)); } } diff --git a/src/factories/MutableUnlockersOvalChainlinkFactory.sol b/src/factories/MutableUnlockersOvalChainlinkFactory.sol index 9c3c0b6..c2865a7 100644 --- a/src/factories/MutableUnlockersOvalChainlinkFactory.sol +++ b/src/factories/MutableUnlockersOvalChainlinkFactory.sol @@ -6,6 +6,9 @@ import {ChainlinkSourceAdapter} from "../adapters/source-adapters/ChainlinkSourc import {ChainlinkDestinationAdapter} from "../adapters/destination-adapters/ChainlinkDestinationAdapter.sol"; import {IAggregatorV3Source} from "../interfaces/chainlink/IAggregatorV3Source.sol"; +/** + * @title OvalChainlinkMutableUnlocker, providing a mutable-unlocker controller for Oval wapped Chainlink. + */ contract OvalChainlinkMutableUnlocker is MutableUnlockersController, ChainlinkSourceAdapter, @@ -26,7 +29,21 @@ contract OvalChainlinkMutableUnlocker is } } +/** + * @title MutableUnlockersOvalChainlinkFactory + * @dev Factory contract to create instances of OvalChainlinkMutableUnlocker. If Oval instances are deployed from this + * factory then downstream contracts can be sure the inheretence structure is defined correctly. + */ contract MutableUnlockersOvalChainlinkFactory { + /** + * @dev Creates an instance of OvalChainlinkMutableUnlocker. + * @param source The Chainlink source aggregator. This is the address of the contract to be wrapped by Oval. + * @param lockWindow The time window during which the unlockers can operate. + * @param maxTraversal The maximum number of historical data points to traverse. + * @param owner The address that will own the created OvalChainlinkMutableUnlocker instance. + * @param unlockers Array of addresses that can unlock the controller. + * @return The address of the newly created OvalChainlinkMutableUnlocker instance. + */ function createMutableUnlockerOvalChainlink( IAggregatorV3Source source, uint256 lockWindow, diff --git a/test/unit/BaseController.sol b/test/unit/BaseController.sol index e02247e..8caffc0 100644 --- a/test/unit/BaseController.sol +++ b/test/unit/BaseController.sol @@ -10,7 +10,7 @@ contract TestBaseController is BaseController, MockSourceAdapter, BaseDestinatio constructor(uint8 decimals) MockSourceAdapter(decimals) BaseController() BaseDestinationAdapter() {} } -contract OvalUnlockLatestValue is CommonTest { +contract BaseControllerTest is CommonTest { uint256 lastUnlockTime = 1690000000; TestBaseController baseController; diff --git a/test/unit/ImmutableController.sol b/test/unit/ImmutableController.sol index bd13b73..4bacf79 100644 --- a/test/unit/ImmutableController.sol +++ b/test/unit/ImmutableController.sol @@ -14,7 +14,7 @@ contract TestImmutableController is ImmutableController, MockSourceAdapter, Base {} } -contract OvalUnlockLatestValue is CommonTest { +contract ImmutableControllerTest is CommonTest { uint8 decimals = 8; uint256 lockWindow = 60; uint256 maxTraversal = 10; @@ -47,28 +47,4 @@ contract OvalUnlockLatestValue is CommonTest { function testMaxTraversalSetCorrectly() public { assertTrue(immutableController.maxTraversal() == maxTraversal); } - - function testCannotSetUnlocker() public { - bytes4 selector = bytes4(keccak256("setUnlocker(address,bool)")); - bytes memory data = abi.encodeWithSelector(selector, random, true); - vm.prank(owner); - (bool success,) = address(immutableController).call(data); - assertFalse(success); - } - - function testCannotSetLockWindow() public { - bytes4 selector = bytes4(keccak256("setLockWindow(uint256)")); - bytes memory data = abi.encodeWithSelector(selector, lockWindow + 1); - vm.prank(owner); - (bool success,) = address(immutableController).call(data); - assertFalse(success); - } - - function testCannotSetMaxTraversal() public { - bytes4 selector = bytes4(keccak256("setMaxTraversal(uint256)")); - bytes memory data = abi.encodeWithSelector(selector, maxTraversal + 1); - vm.prank(owner); - (bool success,) = address(immutableController).call(data); - assertFalse(success); - } } diff --git a/test/unit/ImmutableOvalChainlinkFactory.sol b/test/unit/ImmutableOvalChainlinkFactory.sol new file mode 100644 index 0000000..1984cb0 --- /dev/null +++ b/test/unit/ImmutableOvalChainlinkFactory.sol @@ -0,0 +1,37 @@ +import {ImmutableUnlockersOvalChainlinkFactory} from "../../src/factories/ImmutableOvalChainlinkFactory.sol"; +import {OvalChainlinkImmutable} from "../../src/factories/ImmutableOvalChainlinkFactory.sol"; +import {IAggregatorV3Source} from "../../src/interfaces/chainlink/IAggregatorV3Source.sol"; +import {MockChainlinkV3Aggregator} from "../mocks/MockChainlinkV3Aggregator.sol"; +import {CommonTest} from "../Common.sol"; + +contract ImmutableOvalChainlinkFactoryTest is CommonTest { + ImmutableUnlockersOvalChainlinkFactory factory; + MockChainlinkV3Aggregator mockSource; + address[] unlockers; + uint256 lockWindow = 300; // 5 minutes + uint256 maxTraversal = 15; + + function setUp() public { + mockSource = new MockChainlinkV3Aggregator(8, 420); // 8 decimals + unlockers.push(address(0x123)); + factory = new ImmutableUnlockersOvalChainlinkFactory(); + } + + function testCreateImmutableOvalChainlink() public { + address created = factory.createImmutableOvalChainlink( + IAggregatorV3Source(address(mockSource)), lockWindow, maxTraversal, unlockers + ); + + assertTrue(created != address(0)); // Check if the address is set, non-zero. + + OvalChainlinkImmutable instance = OvalChainlinkImmutable(created); + assertTrue(instance.lockWindow() == lockWindow); + assertTrue(instance.maxTraversal() == maxTraversal); + + // Check if the unlockers are set correctly + for (uint256 i = 0; i < unlockers.length; i++) { + assertTrue(instance.canUnlock(unlockers[i], 0)); + } + assertFalse(instance.canUnlock(address(0x456), 0)); // Check if a random address cannot unlock + } +} diff --git a/test/unit/MutableUnlockersController.sol b/test/unit/MutableUnlockersController.sol new file mode 100644 index 0000000..5abd105 --- /dev/null +++ b/test/unit/MutableUnlockersController.sol @@ -0,0 +1,57 @@ +pragma solidity 0.8.17; + +import {CommonTest} from "../Common.sol"; +import {MutableUnlockersController} from "../../src/controllers/MutableUnlockersController.sol"; +import {MockSourceAdapter} from "../mocks/MockSourceAdapter.sol"; +import {BaseDestinationAdapter} from "../../src/adapters/destination-adapters/BaseDestinationAdapter.sol"; + +contract TestMutableUnlockersController is MutableUnlockersController, MockSourceAdapter, BaseDestinationAdapter { + constructor(address[] memory _unlockers) + MutableUnlockersController(300, 15, _unlockers) + MockSourceAdapter(18) // Assuming 18 decimals for the mock source adapter + BaseDestinationAdapter() + {} +} + +contract MutableUnlockersControllerTest is CommonTest { + TestMutableUnlockersController mutableController; + address[] initialUnlockers; + + function setUp() public { + initialUnlockers.push(permissionedUnlocker); + vm.prank(owner); + mutableController = new TestMutableUnlockersController(initialUnlockers); + } + + function testInitialUnlockersCanUnlock() public { + assertTrue(mutableController.canUnlock(initialUnlockers[0], 0)); + } + + function testNonInitialUnlockerCannotUnlock() public { + assertFalse(mutableController.canUnlock(random, 0)); + } + + function testOwnerCanAddUnlocker() public { + vm.prank(owner); + mutableController.setUnlocker(random, true); + assertTrue(mutableController.canUnlock(random, 0)); + } + + function testOwnerCanRemoveUnlocker() public { + vm.prank(owner); + mutableController.setUnlocker(permissionedUnlocker, false); + assertFalse(mutableController.canUnlock(permissionedUnlocker, 0)); + } + + function testNonOwnerCannotAddUnlocker() public { + vm.prank(random); + vm.expectRevert("Ownable: caller is not the owner"); + mutableController.setUnlocker(random, true); + } + + function testNonOwnerCannotRemoveUnlocker() public { + vm.prank(random); + vm.expectRevert("Ownable: caller is not the owner"); + mutableController.setUnlocker(permissionedUnlocker, false); + } +} diff --git a/test/unit/MutableUnlockersOvalChainlinkFactory.sol b/test/unit/MutableUnlockersOvalChainlinkFactory.sol new file mode 100644 index 0000000..ee06c9c --- /dev/null +++ b/test/unit/MutableUnlockersOvalChainlinkFactory.sol @@ -0,0 +1,53 @@ +import {MutableUnlockersOvalChainlinkFactory} from "../../src/factories/MutableUnlockersOvalChainlinkFactory.sol"; +import {OvalChainlinkMutableUnlocker} from "../../src/factories/MutableUnlockersOvalChainlinkFactory.sol"; +import {IAggregatorV3Source} from "../../src/interfaces/chainlink/IAggregatorV3Source.sol"; +import {MockChainlinkV3Aggregator} from "../mocks/MockChainlinkV3Aggregator.sol"; +import {CommonTest} from "../Common.sol"; + +contract MutableUnlockersOvalChainlinkFactoryTest is CommonTest { + MutableUnlockersOvalChainlinkFactory factory; + MockChainlinkV3Aggregator mockSource; + address[] unlockers; + uint256 lockWindow = 300; + uint256 maxTraversal = 15; + + function setUp() public { + mockSource = new MockChainlinkV3Aggregator(8, 420); + unlockers.push(address(0x123)); + factory = new MutableUnlockersOvalChainlinkFactory(); + } + + function testCreateMutableUnlockerOvalChainlink() public { + address owner = address(this); + address created = factory.createMutableUnlockerOvalChainlink( + IAggregatorV3Source(address(mockSource)), lockWindow, maxTraversal, owner, unlockers + ); + + assertTrue(created != address(0)); // Check if the address is set, non-zero. + + OvalChainlinkMutableUnlocker instance = OvalChainlinkMutableUnlocker(created); + assertTrue(instance.lockWindow() == lockWindow); + assertTrue(instance.maxTraversal() == maxTraversal); + + // Check if the unlockers are set correctly + for (uint256 i = 0; i < unlockers.length; i++) { + assertTrue(instance.canUnlock(unlockers[i], 0)); + } + assertFalse(instance.canUnlock(address(0x456), 0)); // Check if a random address cannot unlock + } + + function testOwnerCanChangeUnlockers() public { + address owner = address(this); + address created = factory.createMutableUnlockerOvalChainlink( + IAggregatorV3Source(address(mockSource)), lockWindow, maxTraversal, owner, unlockers + ); + OvalChainlinkMutableUnlocker instance = OvalChainlinkMutableUnlocker(created); + + address newUnlocker = address(0x789); + instance.setUnlocker(newUnlocker, true); // Correct method to add unlockers + assertTrue(instance.canUnlock(newUnlocker, 0)); + + instance.setUnlocker(address(0x123), false); // Correct method to remove unlockers + assertFalse(instance.canUnlock(address(0x123), 0)); + } +}