Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2 - handler setup #2

Open
wants to merge 2 commits into
base: 1-invariant-setup
Choose a base branch
from
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,27 @@ These contracts are essentially wrappers around the contracts/functionality we w
#### Regressions

If we find a failing sequence, these are the unit tests that we can use to reproduce the failure and check our fix.

### 2 - Handler Setup

branch: `2-handler-setup`

Important Concepts:

#### Handler's purpose

Your handler is the wrapper around the contract that you are testing. You should add all functions you want to be included in your testing to this contract (most likely all external functions that modify state). The test suite will call these functions in random order with random input.

Calls to the contract you're testing should be wrapped in `try/catch` blocks so you can handle errors.

#### Actors

To make the test suite better simulate what will happen post deployment, you need a set of actors and destination addresses that will be the `msg.sender` and targets for your function calls.

#### Error handling

You will need to decide whether you are going to `bound` your inputs so that calls are successful or if you are going to add error exclusions.

#### Requires/Reverts

You can add handler function level assertions to these functions if you want to assert certain things are true during a specific call. This makes the handler functions act like fuzz tests within your invariant test suite.
12 changes: 11 additions & 1 deletion test/invariant/DaiInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@ contract DaiInvariants is Test {
// give the dai handler permission to mint dai
dai.rely(address(_daiHandler));

bytes4[] memory selectors = new bytes4[](0);
// setup Actors in Handler
_daiHandler.init();

bytes4[] memory selectors = new bytes4[](7);
selectors[0] = _daiHandler.transfer.selector;
selectors[1] = _daiHandler.transferFrom.selector;
selectors[2] = _daiHandler.mint.selector;
selectors[3] = _daiHandler.burn.selector;
selectors[4] = _daiHandler.approve.selector;
selectors[5] = _daiHandler.rely.selector;
selectors[6] = _daiHandler.deny.selector;

targetSelector(
FuzzSelector({
Expand Down
199 changes: 199 additions & 0 deletions test/invariant/handlers/DaiHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,206 @@ import {Dai} from "../../../src/dai.sol";
contract DaiHandler is Test {
Dai public dai;

Actor[] public actors;
Actor[] public dsts;
Actor internal actor;

bytes32[] internal expectedErrors;

struct Actor {
address addr;
uint256 key;
}

modifier useRandomActor(uint256 _actorIndex) {
actor = _selectActor(_actorIndex);
changePrank(actor.addr);
_;
delete actor;
vm.stopPrank();
}

modifier resetErrors() {
_;
delete expectedErrors;
}

constructor(Dai dai_) {
dai = dai_;
}

function init() external {
for (uint256 i = 0; i < 10; i++) {
Actor memory _actor;
(_actor.addr, _actor.key) = makeAddrAndKey(string(abi.encodePacked("Actor", vm.toString(i))));
actors.push(_actor);
dsts.push(_actor);
dai.mint(_actor.addr, 1000 ether);
}

Actor memory governance;
(governance.addr, governance.key) = makeAddrAndKey(string(abi.encodePacked("Governance")));
actors.push(governance);
dsts.push(governance);
dai.rely(governance.addr);

Actor memory zero;
(zero.addr, zero.key) = makeAddrAndKey(string(abi.encodePacked("Zero")));
zero.addr = address(0);
dsts.push(zero);
}

// External Handler Functions
function transfer(
uint256 _actorIndex,
uint256 _dstIndex,
uint256 _wad
) public useRandomActor(_actorIndex) resetErrors {
Actor memory dst = _selectDst(_dstIndex);
try dai.transfer(dst.addr, _wad) {
console.log("Transfer succeeded");
} catch Error(string memory reason) {
if(dai.balanceOf(actor.addr) < _wad) addExpectedError("Dai/insufficient-balance");
expectedError(reason);
} catch (bytes memory reason) {
console.log("Transfer failed: ");
console.logBytes(reason);
}
}

function transferFrom(
uint256 _actorIndex,
uint256 _srcIndex,
uint256 _dstIndex,
uint256 _wad
) public useRandomActor(_actorIndex) resetErrors {
Actor memory src = _selectActor(_srcIndex);
Actor memory dst = _selectDst(_dstIndex);
try dai.transferFrom(src.addr, dst.addr, _wad) {
console.log("TransferFrom succeeded");
} catch Error(string memory reason) {
if(dai.balanceOf(src.addr) < _wad) addExpectedError("Dai/insufficient-balance");
if(dai.allowance(actor.addr, src.addr) < _wad) addExpectedError("Dai/insufficient-allowance");
expectedError(reason);
} catch (bytes memory reason) {
console.log("TransferFrom failed: ");
console.logBytes(reason);
}
}

function mint(
uint256 _actorIndex,
uint256 _dstIndex,
uint256 _wad
) public useRandomActor(_actorIndex) resetErrors {
Actor memory dst = _selectDst(_dstIndex);
try dai.mint(dst.addr, _wad) {
console.log("Mint succeeded");
} catch Error(string memory reason) {
if(dai.wards(actor.addr) == 0) addExpectedError("Dai/not-authorized");
expectedError(reason);
} catch (bytes memory reason) {
console.log("Mint failed: ");
console.logBytes(reason);
}
}

function burn(
uint256 _actorIndex,
uint256 _usrIndex,
uint256 _wad
) public useRandomActor(_actorIndex) resetErrors {
Actor memory usr = _selectActor(_usrIndex);
try dai.burn(usr.addr, _wad) {
console.log("burn succeeded");
} catch Error(string memory reason) {
if(dai.balanceOf(usr.addr) < _wad) addExpectedError("Dai/insufficient-balance");
if(dai.allowance(usr.addr, actor.addr) < _wad) addExpectedError("Dai/insufficient-allowance");
expectedError(reason);
} catch (bytes memory reason) {
console.log("burn failed: ");
console.logBytes(reason);
}
}

function approve(
uint256 _actorIndex,
uint256 _usrIndex,
uint256 _wad
) public useRandomActor(_actorIndex) resetErrors {
Actor memory usr = _selectActor(_usrIndex);
try dai.approve(usr.addr, _wad) {
console.log("approve succeeded");
} catch Error(string memory reason) {
expectedError(reason);
} catch (bytes memory reason) {
console.log("approve failed: ");
console.logBytes(reason);
}
}

function rely(
uint256 _actorIndex,
uint256 _guyIndex
) public useRandomActor(_actorIndex) resetErrors {
Actor memory guy = _selectActor(_guyIndex);
try dai.rely(guy.addr) {
console.log("rely succeeded");
} catch Error(string memory reason) {
if(dai.wards(actor.addr) == 0) addExpectedError("Dai/not-authorized");
expectedError(reason);
} catch (bytes memory reason) {
console.log("rely failed: ");
console.logBytes(reason);
}
}

function deny(
uint256 _actorIndex,
uint256 _guyIndex
) public useRandomActor(_actorIndex) resetErrors {
Actor memory guy = _selectActor(_guyIndex);
try dai.deny(guy.addr) {
console.log("deny succeeded");
} catch Error(string memory reason) {
if(dai.wards(actor.addr) == 0) addExpectedError("Dai/not-authorized");
expectedError(reason);
} catch (bytes memory reason) {
console.log("deny failed: ");
console.logBytes(reason);
}
}

// Internal Helper Functions
function _selectActor(uint256 _actorIndex) internal view returns (Actor memory actor_) {
uint256 index = bound(_actorIndex, 0, actors.length - 1);
actor_ = actors[index];
}

function _selectDst(uint256 _dstIndex) internal view returns (Actor memory dst) {
uint256 index = bound(_dstIndex, 0, dsts.length - 1);
dst = dsts[index];
}

function addExpectedError(string memory _err) internal {
expectedErrors.push(keccak256(abi.encodePacked(_err)));
}

function expectedError(string memory _err) internal view {
bytes32 err = keccak256(abi.encodePacked(_err));
bool _valid;

uint256 errLen = expectedErrors.length;
for (uint256 i = 0; i < errLen; i++) {
if (err == expectedErrors[i]) {
_valid = true;
}
}

if (!_valid) {
console.log("Unhandled Error:");
console.log(_err);
}
require(_valid, "Unexpected revert error");
}
}