Skip to content

Commit

Permalink
feat: timelock whitelist (#416)
Browse files Browse the repository at this point in the history
* feat: init timelock whitelist

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

* fix: stack too deep

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

* feat: add basic admin tests

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

* fix: typo

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

* fix: add missing tests

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

* feat: simplify implementation

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

* fix: tests

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

* chore: tiny improvement

Signed-off-by: GopherJ <alex_cj96@foxmail.com>

---------

Signed-off-by: GopherJ <alex_cj96@foxmail.com>
  • Loading branch information
GopherJ authored Sep 20, 2023
1 parent f4cd14a commit 2aa2ecd
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 7 deletions.
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;
}

0 comments on commit 2aa2ecd

Please sign in to comment.