diff --git a/test/Fixture.t.sol b/test/Fixture.t.sol index 3b92a6e..5801cde 100644 --- a/test/Fixture.t.sol +++ b/test/Fixture.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import { IveANGLEVotingDelegation } from "contracts/interfaces/IveANGLEVotingDelegation.sol"; -import { deployMockANGLE, deployVeANGLE } from "../../scripts/test/DeployANGLE.s.sol"; +import { deployMockANGLE, deployVeANGLE } from "../scripts/test/DeployANGLE.s.sol"; import { TimelockController } from "oz/governance/TimelockController.sol"; import { ERC20 } from "oz/token/ERC20/ERC20.sol"; import "contracts/interfaces/IveANGLE.sol"; @@ -56,6 +56,7 @@ contract Fixture is Test { vm.label(sweeper, "Sweeper"); vm.roll(block.number + FORK_BLOCK_NUMBER); + vm.warp(block.timestamp + FORK_BLOCK_TIMSESTAMP); // Deploy necessary contracts - for governance to be deployed diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol index a6481ea..1981b68 100644 --- a/test/invariant/BasicInvariants.t.sol +++ b/test/invariant/BasicInvariants.t.sol @@ -21,7 +21,7 @@ contract BasicInvariants is Fixture { function setUp() public virtual override { super.setUp(); - _voterHandler = new Voter(angleGovernor, _NUM_VOTER); + _voterHandler = new Voter(angleGovernor, ANGLE, _NUM_VOTER); // Label newly created addresses for (uint256 i; i < _NUM_VOTER; i++) diff --git a/test/invariant/DelegationInvariants.t.sol b/test/invariant/DelegationInvariants.t.sol new file mode 100644 index 0000000..edc2131 --- /dev/null +++ b/test/invariant/DelegationInvariants.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.19; + +import { IERC20 } from "oz/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "oz/token/ERC20/extensions/IERC20Metadata.sol"; +import "oz/utils/Strings.sol"; +import { Delegator } from "./actors/Delegator.t.sol"; +import { Param } from "./actors/Param.t.sol"; +import { Fixture, AngleGovernor } from "../Fixture.t.sol"; +import { TimestampStore } from "./stores/TimestampStore.sol"; + +//solhint-disable +import { console } from "forge-std/console.sol"; + +contract DelegationInvariants is Fixture { + uint256 internal constant _NUM_DELEGATORS = 10; + uint256 internal constant _NUM_PARAMS = 1; + + Delegator internal _delegatorHandler; + Param internal _paramHandler; + TimestampStore internal _timestampStore; + + modifier useCurrentTimestamp() { + vm.warp(_timestampStore.currentTimestamp()); + _; + } + + function setUp() public virtual override { + super.setUp(); + + _timestampStore = new TimestampStore(); + _delegatorHandler = new Delegator(_NUM_DELEGATORS, ANGLE, address(veANGLE), address(token), _timestampStore); + _paramHandler = new Param(_NUM_PARAMS, ANGLE, _timestampStore); + + // Label newly created addresses + for (uint256 i; i < _NUM_DELEGATORS; i++) + vm.label(_delegatorHandler.actors(i), string.concat("Delegator ", Strings.toString(i))); + vm.label({ account: address(_timestampStore), newLabel: "TimestampStore" }); + vm.label({ account: address(_paramHandler), newLabel: "Param" }); + + targetContract(address(_delegatorHandler)); + targetContract(address(_paramHandler)); + + { + bytes4[] memory selectors = new bytes4[](5); + selectors[0] = Delegator.delegate.selector; + selectors[1] = Delegator.createLock.selector; + selectors[2] = Delegator.withdraw.selector; + selectors[3] = Delegator.extendLockTime.selector; + selectors[4] = Delegator.extendLockAmount.selector; + targetSelector(FuzzSelector({ addr: address(_delegatorHandler), selectors: selectors })); + } + { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = Param.wrap.selector; + targetSelector(FuzzSelector({ addr: address(_paramHandler), selectors: selectors })); + } + } + + function invariant_RightNumberOfVotesDelegated() public useCurrentTimestamp { + for (uint256 i; i < _NUM_DELEGATORS; i++) { + address actor = _delegatorHandler.actors(i); + + assertEq( + token.delegates(actor), + _delegatorHandler.delegations(actor), + "delegatee should be the same as actor" + ); + } + for (uint256 i; i < _delegatorHandler.delegateesLength(); i++) { + address delegatee = _delegatorHandler.delegatees(i); + uint256 votes = token.getVotes(delegatee); + + uint256 amount = 0; + address[] memory delegators = _delegatorHandler.reverseDelegationsView(delegatee); + for (uint256 j; j < delegators.length; j++) { + address delegator = delegators[j]; + uint256 balance = veANGLE.balanceOf(delegator); + amount += balance; + } + assertEq(votes, amount, "Delegatee should have votes"); + } + } +} diff --git a/test/invariant/actors/BaseActor.t.sol b/test/invariant/actors/BaseActor.t.sol index a3936c9..740d8ab 100644 --- a/test/invariant/actors/BaseActor.t.sol +++ b/test/invariant/actors/BaseActor.t.sol @@ -25,8 +25,7 @@ contract BaseActor is Test { uint256 public nbrActor; address internal _currentActor; - IVotes public agToken; - AngleGovernor internal _angleGovernor; + IERC20 public angle; modifier countCall(bytes32 key) { calls[key]++; @@ -35,17 +34,17 @@ contract BaseActor is Test { modifier useActor(uint256 actorIndexSeed) { _currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)]; - vm.startPrank(_currentActor); + vm.startPrank(_currentActor, _currentActor); _; vm.stopPrank(); } - constructor(uint256 _nbrActor, string memory actorType, AngleGovernor angleGovernor) { + constructor(uint256 _nbrActor, string memory actorType, IERC20 _angle) { for (uint256 i; i < _nbrActor; ++i) { address actor = address(uint160(uint256(keccak256(abi.encodePacked("actor", actorType, i))))); actors.push(actor); } nbrActor = _nbrActor; - _angleGovernor = angleGovernor; + angle = _angle; } } diff --git a/test/invariant/actors/Delegator.t.sol b/test/invariant/actors/Delegator.t.sol new file mode 100644 index 0000000..33cbb12 --- /dev/null +++ b/test/invariant/actors/Delegator.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./BaseActor.t.sol"; +import { IERC5805 } from "oz/interfaces/IERC5805.sol"; +import { MockANGLE } from "../../external/MockANGLE.sol"; +import "contracts/interfaces/IveANGLE.sol"; +import "contracts/utils/Errors.sol"; +import { console } from "forge-std/console.sol"; +import { TimestampStore } from "../stores/TimestampStore.sol"; + +contract Delegator is BaseActor { + IveANGLE public veToken; + IERC5805 public veDelegation; + + mapping(address => address) public delegations; + mapping(address => address[]) public reverseDelegations; + address[] public delegatees; + TimestampStore public timestampStore; + + constructor( + uint256 _nbrActor, + IERC20 _agToken, + address _veToken, + address _veDelegation, + TimestampStore _timestampStore + ) BaseActor(_nbrActor, "Delegator", _agToken) { + veToken = IveANGLE(_veToken); + veDelegation = IERC5805(_veDelegation); + timestampStore = _timestampStore; + } + + function reverseDelegationsView(address locker) public view returns (address[] memory) { + return reverseDelegations[locker]; + } + + function delegateesLength() public view returns (uint256) { + return delegatees.length; + } + + function delegate(uint256 actorIndex, address toDelegate) public useActor(actorIndex) { + if (toDelegate == address(0)) return; + + uint256 balance = veToken.balanceOf(_currentActor); + address currentDelegatee = delegations[_currentActor]; + + if (balance == 0) { + return; + } + + veDelegation.delegate(toDelegate); + timestampStore.increaseCurrentTimestamp(1 weeks); + vm.warp(timestampStore.currentTimestamp()); + + // Update delegations + if (toDelegate == currentDelegatee) { + return; + } + reverseDelegations[toDelegate].push(_currentActor); + for (uint256 i; i < reverseDelegations[currentDelegatee].length; i++) { + if (reverseDelegations[currentDelegatee][i] == _currentActor) { + reverseDelegations[currentDelegatee][i] = reverseDelegations[currentDelegatee][ + reverseDelegations[currentDelegatee].length - 1 + ]; + reverseDelegations[currentDelegatee].pop(); + break; + } + } + delegations[_currentActor] = toDelegate; + for (uint256 i; i < delegatees.length; i++) { + if (delegatees[i] == toDelegate) { + return; + } + } + delegatees.push(toDelegate); + } + + function createLock(uint256 actorIndex, uint256 amount, uint256 duration) public useActor(actorIndex) { + if (veToken.locked__end(_currentActor) != 0) { + return; + } + duration = bound(duration, 1 weeks, 365 days * 4); + amount = bound(amount, 1e18, 100e18); + + MockANGLE(address(angle)).mint(_currentActor, amount); + angle.approve(address(veToken), amount); + + veToken.create_lock(amount, block.timestamp + duration); + } + + function withdraw() public { + if (veToken.locked__end(_currentActor) != 0 && veToken.locked__end(_currentActor) < block.timestamp) { + veToken.withdraw(); + } + } + + function extendLockTime(uint256 actorIndex, uint256 duration) public useActor(actorIndex) { + uint256 end = veToken.locked__end(_currentActor); + if (end == 0 || end < block.timestamp || end + 1 weeks > block.timestamp + 365 days * 4) { + return; + } + + duration = bound(duration, end + 1 weeks, block.timestamp + 365 days * 4); + veToken.increase_unlock_time(duration); + if (delegations[_currentActor] != address(0)) { + veDelegation.delegate(delegations[_currentActor]); + } + } + + function extendLockAmount(uint256 actorIndex, uint256 amount) public useActor(actorIndex) { + if (veToken.balanceOf(_currentActor) == 0) { + return; + } + amount = bound(amount, 1e18, 100e18); + + MockANGLE(address(angle)).mint(_currentActor, amount); + angle.approve(address(veToken), amount); + veToken.increase_amount(amount); + if (delegations[_currentActor] != address(0)) { + veDelegation.delegate(delegations[_currentActor]); + } + } +} diff --git a/test/invariant/actors/Param.t.sol b/test/invariant/actors/Param.t.sol new file mode 100644 index 0000000..149a127 --- /dev/null +++ b/test/invariant/actors/Param.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./BaseActor.t.sol"; +import { IERC5805 } from "oz/interfaces/IERC5805.sol"; +import { MockANGLE } from "../../external/MockANGLE.sol"; +import "contracts/interfaces/IveANGLE.sol"; +import "contracts/utils/Errors.sol"; +import { console } from "forge-std/console.sol"; +import { TimestampStore } from "../stores/TimestampStore.sol"; + +contract Param is BaseActor { + IveANGLE public veToken; + TimestampStore public timestampStore; + + constructor( + uint256 _nbrActor, + IERC20 _agToken, + TimestampStore _timestampStore + ) BaseActor(_nbrActor, "Param", _agToken) { + timestampStore = _timestampStore; + } + + function wrap(uint256 duration) public { + duration = bound(duration, 0, 365 days * 5); + timestampStore.increaseCurrentTimestamp(duration); + vm.warp(timestampStore.currentTimestamp()); + } +} diff --git a/test/invariant/actors/Voter.t.sol b/test/invariant/actors/Voter.t.sol index 794af19..af201f0 100644 --- a/test/invariant/actors/Voter.t.sol +++ b/test/invariant/actors/Voter.t.sol @@ -5,5 +5,9 @@ import { BaseActor, IERC20, IERC20Metadata, AngleGovernor, TestStorage } from ". import { console } from "forge-std/console.sol"; contract Voter is BaseActor { - constructor(AngleGovernor angleGovernor, uint256 nbrVoter) BaseActor(nbrVoter, "Voter", angleGovernor) {} + AngleGovernor internal _angleGovernor; + + constructor(AngleGovernor angleGovernor, IERC20 _agToken, uint256 nbrVoter) BaseActor(nbrVoter, "Voter", _agToken) { + _angleGovernor = angleGovernor; + } } diff --git a/test/invariant/stores/TimestampStore.sol b/test/invariant/stores/TimestampStore.sol new file mode 100644 index 0000000..d214e07 --- /dev/null +++ b/test/invariant/stores/TimestampStore.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.19 <0.9.0; + +/// @dev Because Foundry does not commit the state changes between invariant runs, we need to +/// save the current timestamp in a contract with persistent storage. +contract TimestampStore { + uint256 public currentTimestamp; + + constructor() { + currentTimestamp = block.timestamp; + } + + function increaseCurrentTimestamp(uint256 timeJump) external { + currentTimestamp += timeJump; + } +}