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

feat: timelock whitelist #416

Merged
merged 10 commits into from
Sep 20, 2023
Merged
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
14 changes: 11 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export $(shell sed 's/=.*//' .env)
SCRIPT_PATH := ./scripts/dev/1.ad-hoc.ts
TASK_NAME := print-contracts
TEST_TARGET := *.spec.ts
RUST_TOOLCHAIN := nightly-2022-09-19
RUST_TOOLCHAIN := nightly-2023-05-22

.PHONY: init
init: submodules
Expand Down Expand Up @@ -276,10 +276,18 @@ test-sape-operation:
test-acl-manager:
make TEST_TARGET=acl-manager.spec.ts test

.PHONY: test-time-lock
test-time-lock:
.PHONY: test-timelock-executor
test-timelock-executor:
make TEST_TARGET=time_lock_executor.spec.ts test

.PHONY: test-timelock
test-timelock:
make TEST_TARGET=_timelock.spec.ts test

.PHONY: test-timelock-whitelist
test-timelock-whitelist:
make TEST_TARGET=_timelock_whitelist.spec.ts test

.PHONY: test-stakefish-nft
test-stakefish-nft:
make TEST_TARGET=_stakefish_nft.spec.ts test
Expand Down
33 changes: 33 additions & 0 deletions contracts/interfaces/ITimeLock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ interface ITimeLock {
*/
event TimeLockFrozen(bool value);

/**
* @notice TimeLockWhiteListEvents
* @dev This event is emitted when the time lock whitelist is updated. It provides information about
* addresses that were added to and removed from the whitelist during the update.
*
* @param added An array of addresses that were added to the time lock whitelist.
* @param removed An array of addresses that were removed from the time lock whitelist.
*/
event TimeLockWhitelistUpdated(address[] added, address[] removed);

/** @dev Function to create a new time-lock agreement
* @param assetType Type of the asset involved
* @param actionType Type of action for the time-lock
Expand Down Expand Up @@ -118,4 +128,27 @@ interface ITimeLock {
* @notice This function can only be called by an authorized user
*/
function unfreezeAllAgreements() external;

/**
* @dev Updates the time lock whitelist by adding and/or removing multiple addresses.
* @param toAdd An array of addresses to be added to the whitelist.
* @param toRemove An array of addresses to be removed from the whitelist.
*/
function updateTimeLockWhiteList(
address[] calldata toAdd,
address[] calldata toRemove
) external;

/**
* @notice TimeLockWhiteList
* @dev This function allows external callers to check whether an array of addresses are
* on the time lock whitelist, indicating whether they have permission for specific actions.
*
* @param users An array of addresses to check for whitelist membership.
* @return isWhiteListed An array of boolean values indicating whether each provided address
* is on the time lock whitelist (true if whitelisted, false otherwise).
*/
function isTimeLockWhiteListed(
address[] calldata users
) external view returns (bool[] memory);
}
40 changes: 39 additions & 1 deletion contracts/misc/TimeLock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ contract TimeLock is ITimeLock, ReentrancyGuardUpgradeable, IERC721Receiver {
uint248 public agreementCount;
bool public frozen;

// TimeLock whitelist
mapping(address => bool) _whiteList;

IPool private immutable POOL;
IACLManager private immutable ACL_MANAGER;
address private immutable weth;
address private immutable wpunk;
address private immutable Punk;

uint48 private constant MIN_WAIT_TIME = 12;

modifier onlyXToken(address asset) {
require(
msg.sender == POOL.getReserveXToken(asset),
Expand Down Expand Up @@ -83,7 +88,11 @@ contract TimeLock is ITimeLock, ReentrancyGuardUpgradeable, IERC721Receiver {
uint48 releaseTime
) external onlyXToken(asset) returns (uint256) {
require(beneficiary != address(0), "Beneficiary cant be zero address");
require(releaseTime > block.timestamp, "Release time not valid");
if (_whiteList[beneficiary]) {
releaseTime = uint48(block.timestamp) + MIN_WAIT_TIME;
} else {
require(releaseTime > block.timestamp, "Release time not valid");
}

uint256 agreementId = agreementCount++;
agreements[agreementId] = Agreement({
Expand Down Expand Up @@ -265,4 +274,33 @@ contract TimeLock is ITimeLock, ReentrancyGuardUpgradeable, IERC721Receiver {
) external virtual override returns (bytes4) {
return this.onERC721Received.selector;
}

/// @inheritdoc ITimeLock
function updateTimeLockWhiteList(
address[] calldata toAdd,
address[] calldata toRemove
) external onlyPoolAdmin {
for (uint256 i = 0; i < toAdd.length; i++) {
if (!_whiteList[toAdd[i]]) {
_whiteList[toAdd[i]] = true;
}
}
for (uint256 i = 0; i < toRemove.length; i++) {
if (_whiteList[toRemove[i]]) {
_whiteList[toRemove[i]] = false;
}
}
emit TimeLockWhitelistUpdated(toAdd, toRemove);
}

/// @inheritdoc ITimeLock
function isTimeLockWhiteListed(
address[] calldata users
) external view returns (bool[] memory) {
bool[] memory res = new bool[](users.length);
for (uint256 i = 0; i < users.length; i++) {
res[i] = _whiteList[users[i]];
}
return res;
}
}
2 changes: 1 addition & 1 deletion rust-toolchain
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nightly-2022-09-19
nightly-2023-05-22
72 changes: 70 additions & 2 deletions test/_timelock.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {loadFixture} from "@nomicfoundation/hardhat-network-helpers";
import {expect} from "chai";
import {deployReserveTimeLockStrategy} from "../helpers/contracts-deployments";
import {MAX_UINT_AMOUNT} from "../helpers/constants";
import {MAX_UINT_AMOUNT, ONE_ADDRESS} from "../helpers/constants";
import {
getInitializableAdminUpgradeabilityProxy,
getPoolConfiguratorProxy,
getTimeLockProxy,
} from "../helpers/contracts-getters";
import {convertToCurrencyDecimals} from "../helpers/contracts-helpers";
import {advanceTimeAndBlock, waitForTx} from "../helpers/misc-utils";
import {eContractid} from "../helpers/types";
import {eContractid, ProtocolErrors} from "../helpers/types";
import {testEnvFixture} from "./helpers/setup-env";
import {supplyAndValidate} from "./helpers/validated-steps";
import {parseEther} from "ethers/lib/utils";
Expand Down Expand Up @@ -504,4 +505,71 @@ describe("TimeLock functionality tests", () => {

await expect(balanceAfter).to.be.eq(balanceBefore.add(3));
});

it("non-pool admin cannot update timeLockWhiteList", async () => {
const {
users: [user1],
timeLock,
} = await loadFixture(fixture);
await expect(
timeLock.updateTimeLockWhiteList([user1.address], [])
).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN);
await expect(
timeLock.updateTimeLockWhiteList([], [user1.address])
).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN);
});

it("pool admin can update timeLockWhiteList", async () => {
const {
users: [user1, user2],
poolAdmin,
timeLock,
} = await loadFixture(fixture);
await expect(await timeLock.isTimeLockWhiteListed([user1.address])).deep.eq(
[false]
);
await waitForTx(
await (await getInitializableAdminUpgradeabilityProxy(timeLock.address))
.connect(poolAdmin.signer)
.changeAdmin(ONE_ADDRESS)
);
await waitForTx(
await timeLock
.connect(poolAdmin.signer)
.updateTimeLockWhiteList([user1.address], [])
);
await expect(await timeLock.isTimeLockWhiteListed([user1.address])).deep.eq(
[true]
);

await waitForTx(
await timeLock
.connect(poolAdmin.signer)
.updateTimeLockWhiteList([], [user1.address])
);
await expect(await timeLock.isTimeLockWhiteListed([user1.address])).deep.eq(
[false]
);

await waitForTx(
await timeLock
.connect(poolAdmin.signer)
.updateTimeLockWhiteList([user1.address], [user1.address])
);
await expect(
await timeLock.isTimeLockWhiteListed([user1.address, user2.address])
).deep.eq([false, false]);

await waitForTx(
await timeLock
.connect(poolAdmin.signer)
.updateTimeLockWhiteList(
[user1.address, user2.address],
[user1.address]
)
);
await expect(
await timeLock.isTimeLockWhiteListed([user1.address, user2.address])
).deep.eq([false, true]);
});
});
120 changes: 120 additions & 0 deletions test/_timelock_whitelist.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {expect} from "chai";
import {BigNumber} from "ethers";
import {timeLatest, waitForTx} from "../helpers/misc-utils";
import {loadFixture} from "@nomicfoundation/hardhat-network-helpers";
import {deployReserveTimeLockStrategy} from "../helpers/contracts-deployments";
import {testEnvFixture} from "./helpers/setup-env";
import {TestEnv} from "./helpers/make-suite";
import {DefaultTimeLockStrategy} from "../types";
import {eContractid} from "../helpers/types";
import {supplyAndValidate} from "./helpers/validated-steps";
import {almostEqual} from "./helpers/uniswapv3-helper";
import {convertToCurrencyDecimals} from "../helpers/contracts-helpers";
import {getInitializableAdminUpgradeabilityProxy} from "../helpers/contracts-getters";
import {ONE_ADDRESS} from "../helpers/constants";

describe("timeLock whiteList tests", function () {
let defaultTimeLockStrategy: DefaultTimeLockStrategy;
let testEnv: TestEnv;

const minThreshold = BigNumber.from(10);
const midThreshold = BigNumber.from(20);
const minWaitTime = 100;
const midWaitTime = 2000;
const maxWaitTime = 36000;
const maxPoolPeriodRate = BigNumber.from(400);
const maxPoolPeriodWaitTime = 40;
const period = 86400;

const fixture = async () => {
testEnv = await loadFixture(testEnvFixture);
const {
configurator,
pool,
usdc,
users: [user1, user2],
} = testEnv;

defaultTimeLockStrategy = await deployReserveTimeLockStrategy(
eContractid.DefaultTimeLockStrategy,
pool.address,
minThreshold.toString(),
midThreshold.toString(),
minWaitTime.toString(),
midWaitTime.toString(),
maxWaitTime.toString(),
maxPoolPeriodRate.toString(),
maxPoolPeriodWaitTime.toString(),
period.toString()
);

await supplyAndValidate(usdc, "10000", user1, true);
await supplyAndValidate(usdc, "10000", user2, true);

await waitForTx(
await configurator.setReserveTimeLockStrategyAddress(
usdc.address,
defaultTimeLockStrategy.address
)
);

return testEnv;
};

it("normal non-whitelisted user should still have long wait time", async function () {
const {
pool,
users: [user1],
usdc,
timeLock,
} = await loadFixture(fixture);
const currentTime = (await timeLatest()).toNumber();
await waitForTx(
await pool
.connect(user1.signer)
.borrow(
usdc.address,
await convertToCurrencyDecimals(usdc.address, "1000"),
0,
user1.address
)
);
await expect(
(await timeLock.getAgreement(0)).releaseTime - currentTime
).to.gte(minWaitTime);
});

it("whitelisted user should only have 12s of wait time", async function () {
const {
pool,
users: [user1],
usdc,
timeLock,
poolAdmin,
} = await loadFixture(fixture);
await waitForTx(
await (await getInitializableAdminUpgradeabilityProxy(timeLock.address))
.connect(poolAdmin.signer)
.changeAdmin(ONE_ADDRESS)
);
await waitForTx(
await timeLock
.connect(poolAdmin.signer)
.updateTimeLockWhiteList([user1.address], [])
);
await waitForTx(
await pool
.connect(user1.signer)
.withdraw(
usdc.address,
await convertToCurrencyDecimals(usdc.address, "1000"),
user1.address
)
);
const currentTime = (await timeLatest()).toNumber();
await almostEqual(
(await timeLock.getAgreement(0)).releaseTime - currentTime,
12
);
});
});
5 changes: 5 additions & 0 deletions test/helpers/make-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
getNTokenOtherdeed,
getStakefishNFTManager,
getNTokenStakefish,
getTimeLockProxy,
} from "../../helpers/contracts-getters";
import {
eContractid,
Expand Down Expand Up @@ -88,6 +89,7 @@ import {
SeaportAdapter,
StakefishNFTManager,
StETHDebtToken,
TimeLock,
UiPoolDataProvider,
WstETHMocked,
X2Y2Adapter,
Expand Down Expand Up @@ -218,6 +220,7 @@ export interface TestEnv {
executionDelegate: ExecutionDelegate;
blurExchange: BlurExchange;
blurAdapter: BlurAdapter;
timeLock: TimeLock;
}

export async function initializeMakeSuite() {
Expand Down Expand Up @@ -290,6 +293,7 @@ export async function initializeMakeSuite() {
executionDelegate: {} as ExecutionDelegate,
blurExchange: {} as BlurExchange,
blurAdapter: {} as BlurAdapter,
timeLock: {} as TimeLock,
} as TestEnv;
const paraSpaceConfig = getParaSpaceConfig();
const signers = await Promise.all(
Expand Down Expand Up @@ -566,6 +570,7 @@ export async function initializeMakeSuite() {
testEnv.executionDelegate = await getExecutionDelegate();
testEnv.blurExchange = await getBlurExchangeProxy();
testEnv.blurAdapter = await getBlurAdapter();
testEnv.timeLock = await getTimeLockProxy();

return testEnv;
}
Loading