Skip to content

Commit

Permalink
feat: base of vedelegation invariant tests (#12)
Browse files Browse the repository at this point in the history
* feat: base of vedelegation invariant tests

* refactor: improve the invariant tests

* refactor: doesn't use anymore the lockAmounts

* wip: delegator invariant failing

* tests: use TimestampStore for invariant tests

* tests: add Param handler

* tests: add extendLock amount and duration for delegation

* chore: remove useless params in foundry.toml
  • Loading branch information
0xtekgrinder authored Dec 11, 2023
1 parent 2a69d47 commit f31c4e3
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 8 deletions.
3 changes: 2 additions & 1 deletion test/Fixture.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/invariant/BasicInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
Expand Down
85 changes: 85 additions & 0 deletions test/invariant/DelegationInvariants.t.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
9 changes: 4 additions & 5 deletions test/invariant/actors/BaseActor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]++;
Expand All @@ -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;
}
}
123 changes: 123 additions & 0 deletions test/invariant/actors/Delegator.t.sol
Original file line number Diff line number Diff line change
@@ -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]);
}
}
}
29 changes: 29 additions & 0 deletions test/invariant/actors/Param.t.sol
Original file line number Diff line number Diff line change
@@ -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());
}
}
6 changes: 5 additions & 1 deletion test/invariant/actors/Voter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
16 changes: 16 additions & 0 deletions test/invariant/stores/TimestampStore.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit f31c4e3

Please sign in to comment.