packages/contracts-core/test/suite/manager/BondingManager.t.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {InterfaceOrigin} from "../../../contracts/interfaces/InterfaceOrigin.sol";
import {AGENT_TREE_HEIGHT, FRESH_DATA_TIMEOUT} from "../../../contracts/libs/Constants.sol";
import {
AgentCantBeAdded,
AgentNotActive,
AgentNotUnstaking,
CallerNotSummit,
DisputeAlreadyResolved,
DisputeNotOpened,
IncorrectAgentDomain,
MustBeSynapseDomain,
IncorrectOriginDomain,
NotStuck,
SlashAgentOptimisticPeriod,
SynapseDomainForbidden
} from "../../../contracts/libs/Errors.sol";
import {MerkleMath} from "../../../contracts/libs/merkle/MerkleMath.sol";
import {AgentFlag, DisputeFlag} from "../../../contracts/libs/Structures.sol";
import {AgentManagerTest} from "./AgentManager.t.sol";
import {BondingManager, BondingManagerHarness, SynapseTest} from "../../utils/SynapseTest.t.sol";
import {Random} from "../../utils/libs/Random.t.sol";
// solhint-disable func-name-mixedcase
// solhint-disable no-empty-blocks
// solhint-disable ordering
contract BondingManagerTest is AgentManagerTest {
// Deploy mocks for everything except BondingManager and Inbox
constructor() SynapseTest(0) {}
// ═══════════════════════════════════════════════ TESTS: SETUP ════════════════════════════════════════════════════
function test_cleanSetup(Random memory random) public override {
uint32 domain = DOMAIN_SYNAPSE;
vm.chainId(domain);
address caller = random.nextAddress();
address origin_ = random.nextAddress();
address destination_ = random.nextAddress();
address summit_ = random.nextAddress();
address inbox_ = random.nextAddress();
BondingManager cleanContract = new BondingManager(domain);
vm.prank(caller);
cleanContract.initialize(origin_, destination_, inbox_, summit_);
assertEq(cleanContract.localDomain(), domain);
assertEq(cleanContract.owner(), caller);
assertEq(cleanContract.origin(), origin_);
assertEq(cleanContract.destination(), destination_);
assertEq(cleanContract.inbox(), inbox_);
assertEq(cleanContract.summit(), summit_);
assertEq(cleanContract.leafsAmount(), 1);
}
function initializeLocalContract() public override {
BondingManager(localContract()).initialize(address(0), address(0), address(0), address(0));
}
function test_constructor_revert_notOnSynapseChain(uint32 domain) public {
vm.assume(domain != DOMAIN_SYNAPSE);
vm.expectRevert(MustBeSynapseDomain.selector);
new BondingManager(DOMAIN_SYNAPSE);
}
function test_constructor_revert_chainIdOverflow() public {
vm.chainId(2 ** 32);
vm.expectRevert("SafeCast: value doesn't fit in 32 bits");
new BondingManager(DOMAIN_SYNAPSE);
}
function test_setup() public override {
super.test_setup();
assertEq(bondingManager.summit(), localSummit(), "!summit");
assertEq(bondingManager.version(), LATEST_VERSION, "!version");
}
// ═══════════════════════════════════════ TESTS: RESOLVE STUCK DISPUTES ═══════════════════════════════════════════
function test_resolveDisputeWhenStuck(Random memory random, uint256 timePassed) public {
address guard = randomGuard(random);
address notary = randomNotary(random);
openDispute(guard, notary);
timePassed = FRESH_DATA_TIMEOUT + (timePassed % 1 days);
mockSnapRootTime(timePassed);
address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary;
address rival = slashedAgent == guard ? notary : guard;
expectStatusUpdated(AgentFlag.Fraudulent, agentDomain[slashedAgent], slashedAgent);
expectDisputeResolved(1, slashedAgent, rival, address(0));
bondingManager.resolveDisputeWhenStuck(agentDomain[slashedAgent], slashedAgent);
checkDisputeStatus(slashedAgent, DisputeFlag.Slashed, rival, address(0), 1);
checkDisputeStatus(rival, DisputeFlag.None, address(0), address(0), 0);
}
function test_resolveDisputeWhenStuck_revert_callerNotOwner(address caller) public {
vm.assume(caller != testedAM().owner());
expectRevertNotOwner();
vm.prank(caller);
bondingManager.resolveDisputeWhenStuck(0, address(0));
}
function test_resolveDisputeWhenStuck_revert_notStuck(Random memory random, uint256 timePassed) public {
address guard = randomGuard(random);
address notary = randomNotary(random);
openDispute(guard, notary);
timePassed = timePassed % FRESH_DATA_TIMEOUT;
mockSnapRootTime(timePassed);
address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary;
vm.expectRevert(NotStuck.selector);
bondingManager.resolveDisputeWhenStuck(agentDomain[slashedAgent], slashedAgent);
}
function test_resolveDisputeWhenStuck_revert_agentNotDispute(Random memory random) public {
address guard0 = getGuard(0);
address notary = randomNotary(random);
openDispute(guard0, notary);
address guard1 = getGuard(1);
mockSnapRootTime(FRESH_DATA_TIMEOUT);
vm.expectRevert(DisputeNotOpened.selector);
bondingManager.resolveDisputeWhenStuck(agentDomain[guard1], guard1);
}
function test_resolveDisputeWhenStuck_revert_alreadyResolved(Random memory random) public {
address guard = randomGuard(random);
address notary = randomNotary(random);
openDispute(guard, notary);
address slashedByInbox = random.nextUint256() % 2 == 0 ? guard : notary;
vm.prank(localInbox());
bondingManager.slashAgent(agentDomain[slashedByInbox], slashedByInbox, address(0));
address slashedByOwner = random.nextUint256() % 2 == 0 ? guard : notary;
mockSnapRootTime(FRESH_DATA_TIMEOUT);
vm.expectRevert(slashedByInbox == slashedByOwner ? DisputeAlreadyResolved.selector : DisputeNotOpened.selector);
bondingManager.resolveDisputeWhenStuck(agentDomain[slashedByOwner], slashedByOwner);
}
function test_resolveDisputeWhenStuck_revert_incorrectDomain(Random memory random, uint32 incorrectDomain) public {
address guard = randomGuard(random);
address notary = randomNotary(random);
openDispute(guard, notary);
address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary;
vm.assume(incorrectDomain != agentDomain[slashedAgent]);
mockSnapRootTime(FRESH_DATA_TIMEOUT);
vm.expectRevert(IncorrectAgentDomain.selector);
bondingManager.resolveDisputeWhenStuck(incorrectDomain, slashedAgent);
}
// ══════════════════════════════════ TESTS: UNAUTHORIZED ACCESS (NOT OWNER) ═══════════════════════════════════════
function test_addAgent_revert_notOwner(address caller) public {
vm.assume(caller != address(this));
expectRevertNotOwner();
vm.prank(caller);
bondingManager.addAgent(1, address(1), new bytes32[](0));
}
function test_initiateUnstaking_revert_notOwner(address caller) public {
vm.assume(caller != address(this));
expectRevertNotOwner();
vm.prank(caller);
bondingManager.initiateUnstaking(1, address(1), new bytes32[](0));
}
function test_completeUnstaking_revert_notOwner(address caller) public {
vm.assume(caller != address(this));
expectRevertNotOwner();
vm.prank(caller);
bondingManager.completeUnstaking(1, address(1), new bytes32[](0));
}
// ═════════════════════════════════════════ TESTS: ADD/REMOVE AGENTS ══════════════════════════════════════════════
function test_addAgent_fromScratch() public {
// Deploy fresh instance of BondingManager
vm.chainId(DOMAIN_SYNAPSE);
bondingManager = new BondingManagerHarness(DOMAIN_SYNAPSE);
bondingManager.initialize(originSynapse, destinationSynapse, address(inbox), summit);
// Try to add all agents one by one
for (uint256 d = 0; d < allDomains.length; ++d) {
uint32 domain = allDomains[d];
for (uint256 i = 0; i < DOMAIN_AGENTS; ++i) {
address agent = domains[domain].agents[i];
bytes32[] memory proof = bondingManager.getProof(agent);
bondingManager.addAgent(domain, agent, proof);
checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Active);
}
}
}
function test_addAgent_new(uint32 domain, address agent) public {
// Notaries on Syn Chain could nto be added
vm.assume(domain != DOMAIN_SYNAPSE);
// Should not be an already added agent
vm.assume(bondingManager.agentStatus(agent).flag == AgentFlag.Unknown);
vm.assume(agent != address(0));
bytes32[] memory proof = getZeroProof();
bytes32 newRoot = addNewAgent(domain, agent);
expectStatusUpdated(AgentFlag.Active, domain, agent);
vm.expectEmit();
emit RootUpdated(newRoot);
bondingManager.addAgent(domain, agent, proof);
checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Active);
assertEq(bondingManager.agentRoot(), newRoot, "!agentRoot");
}
function test_addAgent_revert_synapseDomain(address agent) public {
bytes32[] memory proof = getZeroProof();
vm.expectRevert(SynapseDomainForbidden.selector);
bondingManager.addAgent(DOMAIN_SYNAPSE, agent, proof);
}
function test_addAgent_resting(uint256 domainId, uint256 agentId) public {
// Full lifecycle for a live agent:
// Active -> Unstaking -> Resting -> Active
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
updateStatus(AgentFlag.Resting, domain, agent);
updateStatus(AgentFlag.Active, domain, agent);
}
function test_initiateUnstaking(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
}
function test_completeUnstaking(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
updateStatus(AgentFlag.Resting, domain, agent);
}
function updateStatus(AgentFlag flag, uint32 domain, address agent) public {
updateStatus(address(this), flag, domain, agent);
}
function updateStatus(address caller, AgentFlag flag, uint32 domain, address agent) public {
bytes32[] memory proof = getAgentProof(agent);
bytes32 newRoot = updateAgent(flag, agent);
expectStatusUpdated(flag, domain, agent);
vm.expectEmit();
emit RootUpdated(newRoot);
vm.prank(caller);
updateStatusWithProof(flag, domain, agent, proof);
assertEq(bondingManager.agentRoot(), newRoot, "!agentRoot");
checkAgentStatus(agent, bondingManager.agentStatus(agent), flag);
}
function updateStatusWithProof(AgentFlag flag, uint32 domain, address agent, bytes32[] memory proof) public {
if (flag == AgentFlag.Unstaking) {
bondingManager.initiateUnstaking(domain, agent, proof);
} else if (flag == AgentFlag.Resting) {
bondingManager.completeUnstaking(domain, agent, proof);
} else if (flag == AgentFlag.Active) {
bondingManager.addAgent(domain, agent, proof);
} else if (flag == AgentFlag.Slashed) {
bondingManager.completeSlashing(domain, agent, proof);
}
}
// ═══════════════════════════════════════ TEST: UPDATE AGENTS (REVERTS) ═══════════════════════════════════════════
function test_addAgent_revert_active(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatusWithRevert(AgentFlag.Active, domain, agent, AgentCantBeAdded.selector);
}
function test_addAgent_revert_unstaking(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
updateStatusWithRevert(AgentFlag.Active, domain, agent, AgentCantBeAdded.selector);
}
function test_initiateUnstaking_revert_unstaking(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
updateStatusWithRevert(AgentFlag.Unstaking, domain, agent, AgentNotActive.selector);
}
function test_initiateUnstaking_revert_resting(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
updateStatus(AgentFlag.Resting, domain, agent);
updateStatusWithRevert(AgentFlag.Unstaking, domain, agent, AgentNotActive.selector);
}
function test_completeUnstaking_revert_active(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatusWithRevert(AgentFlag.Resting, domain, agent, AgentNotUnstaking.selector);
}
function test_completeUnstaking_revert_resting(uint256 domainId, uint256 agentId) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
updateStatus(AgentFlag.Resting, domain, agent);
updateStatusWithRevert(AgentFlag.Resting, domain, agent, AgentNotUnstaking.selector);
}
function updateStatusWithRevert(AgentFlag flag, uint32 domain, address agent, bytes4 err) public {
bytes32[] memory proof = getAgentProof(agent);
vm.expectRevert(err);
updateStatusWithProof(flag, domain, agent, proof);
}
// ═══════════════════════════════════════════ TEST: SLASHING AGENTS ═══════════════════════════════════════════════
// TODO: test_initiateSlashing
function test_remoteSlashAgent(uint32 msgOrigin, uint256 domainId, uint256 agentId, address prover) public {
// Needs to be a REMOTE call
vm.assume(msgOrigin != DOMAIN_SYNAPSE);
(uint32 domain, address agent) = getAgent(domainId, agentId);
skipBondingOptimisticPeriod();
bytes memory msgPayload = managerMsgPayload(msgOrigin, remoteSlashAgentCalldata(domain, agent, prover));
expectStatusUpdated(AgentFlag.Fraudulent, domain, agent);
expectDisputeResolved(0, agent, address(0), prover);
managerMsgPrank(msgPayload);
assertEq(uint8(bondingManager.agentStatus(agent).flag), uint8(AgentFlag.Fraudulent));
// (bool isSlashed, address prover_) = bondingManager.slashStatus(agent);
// assertTrue(isSlashed);
// assertEq(prover_, prover);
}
function test_remoteSlashAgent_revert_optimisticPeriodNotOver(uint32 proofMaturity) public {
proofMaturity = proofMaturity % BONDING_OPTIMISTIC_PERIOD;
skipPeriod(proofMaturity);
bytes memory msgPayload = managerMsgPayload(DOMAIN_REMOTE, remoteSlashAgentCalldata(0, address(0), address(0)));
vm.expectRevert(SlashAgentOptimisticPeriod.selector);
managerMsgPrank(msgPayload);
}
function test_remoteSlashAgent_revert_optimisticPeriodMinus1Second() public {
test_remoteSlashAgent_revert_optimisticPeriodNotOver(BONDING_OPTIMISTIC_PERIOD - 1);
}
function test_remoteSlashAgent_revert_sameOriginDomain() public {
uint32 proofMaturity = BONDING_OPTIMISTIC_PERIOD;
skip(proofMaturity);
bytes memory msgPayload = managerMsgPayload(DOMAIN_SYNAPSE, remoteSlashAgentCalldata(0, address(0), address(0)));
vm.expectRevert(IncorrectOriginDomain.selector);
managerMsgPrank(msgPayload);
}
function test_completeSlashing_active(uint256 domainId, uint256 agentId, address slasher) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
// Initiate slashing
test_remoteSlashAgent(DOMAIN_REMOTE, domainId, agentId, address(1));
updateStatus(slasher, AgentFlag.Slashed, domain, agent);
checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Slashed);
}
function test_completeSlashing_unstaking(uint256 domainId, uint256 agentId, address slasher) public {
(uint32 domain, address agent) = getAgent(domainId, agentId);
updateStatus(AgentFlag.Unstaking, domain, agent);
// Initiate slashing
test_remoteSlashAgent(DOMAIN_REMOTE, domainId, agentId, address(1));
updateStatus(slasher, AgentFlag.Slashed, domain, agent);
checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Slashed);
}
// ════════════════════════════════════════════ TEST: WITHDRAW TIPS ════════════════════════════════════════════════
function test_withdrawTips_local(address recipient, uint256 amount) public {
bytes memory expectedCall = abi.encodeWithSelector(InterfaceOrigin.withdrawTips.selector, recipient, amount);
vm.expectCall(originSynapse, expectedCall);
vm.prank(summit);
bondingManager.withdrawTips(recipient, DOMAIN_SYNAPSE, amount);
}
function test_withdrawTips_remote(address recipient, uint32 domain, uint256 amount) public {
vm.assume(domain != DOMAIN_SYNAPSE);
// remoteWithdrawTips(msgOrigin, proofMaturity, recipient, amount), but first two are omitted
bytes memory payload = abi.encodeWithSelector(lightManager.remoteWithdrawTips.selector, recipient, amount);
// sendManagerMessage(destination, optimisticPeriod, payload)
bytes memory expectedCall = abi.encodeWithSelector(
InterfaceOrigin.sendManagerMessage.selector, domain, BONDING_OPTIMISTIC_PERIOD, payload
);
vm.expectCall(address(originSynapse), expectedCall);
vm.prank(summit);
bondingManager.withdrawTips(recipient, domain, amount);
}
function test_withdrawTips_revert_notSummit(address caller) public {
vm.assume(caller != summit);
vm.expectRevert(CallerNotSummit.selector);
vm.prank(caller);
bondingManager.withdrawTips(address(0), 0, 0);
}
// ════════════════════════════════════════════════ TEST: VIEWS ════════════════════════════════════════════════════
function test_agentLeaf_knownAgent(uint256 domainId, uint256 agentId) public {
(, address agent) = getAgent(domainId, agentId);
assertEq(bondingManager.agentLeaf(agent), getAgentLeaf(agentIndex[agent]));
}
function test_agentLeaf_unknownAgent(address agent) public {
// Should not be an already added agent
vm.assume(bondingManager.agentStatus(agent).flag == AgentFlag.Unknown);
assertEq(bondingManager.agentLeaf(agent), bytes32(0));
}
function test_getActiveAgents() public {
for (uint256 d = 0; d < allDomains.length; ++d) {
uint32 domain = allDomains[d];
address[] memory agents = bondingManager.getActiveAgents(domain);
assertEq(agents.length, DOMAIN_AGENTS);
for (uint256 i = 0; i < agents.length; ++i) {
assertEq(agents[i], domains[domain].agents[i]);
}
}
}
function test_getActiveAgents_agentsRemoved() public {
// Change status of four agents into Unstaking, Resting, Fraudulent and Slashed - one for each domain
test_initiateUnstaking(0, 0);
test_completeUnstaking(1, 1);
test_remoteSlashAgent(DOMAIN_REMOTE, 2, 2, address(1));
test_completeSlashing_active(3, 3, address(1));
for (uint256 d = 0; d < allDomains.length; ++d) {
uint32 domain = allDomains[d];
address[] memory agents = bondingManager.getActiveAgents(domain);
assertEq(agents.length, DOMAIN_AGENTS - 1);
for (uint256 i = 0; i < agents.length; ++i) {
// Agent with index `d` was removed
assertEq(agents[i], domains[domain].agents[i < d ? i : i + 1]);
}
}
}
function test_getProof_knownAgent(uint256 domainId, uint256 agentId) public {
(, address agent) = getAgent(domainId, agentId);
bytes32[] memory proof = bondingManager.getProof(agent);
uint256 index = agentIndex[agent];
checkProof(index, proof);
}
function test_getProof_unknownAgent(address agent) public {
// Should not be an already added agent
vm.assume(bondingManager.agentStatus(agent).flag == AgentFlag.Unknown);
bytes32[] memory proof = bondingManager.getProof(agent);
// Use the next index
uint256 index = totalAgents + 1;
checkProof(index, proof);
}
function checkProof(uint256 index, bytes32[] memory proof) public {
assertEq(MerkleMath.proofRoot(index, getAgentLeaf(index), proof, AGENT_TREE_HEIGHT), getAgentRoot());
}
function test_allLeafs() public {
assertEq(bondingManager.leafsAmount(), totalAgents + 1, "!leafsAmount");
bytes32[] memory leafs = bondingManager.allLeafs();
for (uint256 i = 0; i < leafs.length; ++i) {
assertEq(leafs[i], getAgentLeaf(i));
}
}
function test_getLeafs(uint256 indexFrom, uint256 amount) public {
uint256 totalLeafs = totalAgents + 1;
indexFrom = indexFrom % totalLeafs;
// Allow index overrun
amount = amount % (totalLeafs + 10);
bytes32[] memory leafs = bondingManager.getLeafs(indexFrom, amount);
if (indexFrom + amount <= totalLeafs) {
assertEq(leafs.length, amount);
} else {
assertEq(leafs.length, totalLeafs - indexFrom);
}
for (uint256 i = 0; i < leafs.length; ++i) {
assertEq(leafs[i], getAgentLeaf(indexFrom + i));
}
}
// ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════
/// @notice Returns local domain for the tested contract
function localDomain() public pure override returns (uint32) {
return DOMAIN_SYNAPSE;
}
}